Statistics
| Branch: | Tag: | Revision:

root / qa / qa_utils.py @ 33c730a2

History | View | Annotate | Download (20.8 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 os
27
import re
28
import sys
29
import subprocess
30
import random
31
import tempfile
32
import operator
33

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

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

    
46
import qa_config
47
import qa_error
48

    
49

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

    
55
_MULTIPLEXERS = {}
56

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

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

    
63

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

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

    
70

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

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

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

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

    
89
  curses.setupterm()
90

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

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

    
98

    
99
_SetupColours()
100

    
101

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

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

    
109

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

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

    
117

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

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

    
125

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

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

    
133

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

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

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

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

    
149
  return result
150

    
151

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

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

    
163

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

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

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

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

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

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

    
193
  return rcode
194

    
195

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

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

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

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

    
216

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

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

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

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

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

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

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

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

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

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

    
274
  return args
275

    
276

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

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

    
289

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

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

    
297

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

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

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

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

    
315

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

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

    
325

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

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

    
345

    
346
def UploadFile(node, src):
347
  """Uploads a file to a node and returns the filename.
348

349
  Caller needs to remove the returned file on the node when it's not needed
350
  anymore.
351

352
  """
353
  # Make sure nobody else has access to it while preserving local permissions
354
  mode = os.stat(src).st_mode & 0700
355

    
356
  cmd = ('tmp=$(tempfile --mode %o --prefix gnt) && '
357
         '[[ -f "${tmp}" ]] && '
358
         'cat > "${tmp}" && '
359
         'echo "${tmp}"') % mode
360

    
361
  f = open(src, "r")
362
  try:
363
    p = subprocess.Popen(GetSSHCommand(node, cmd), shell=False, stdin=f,
364
                         stdout=subprocess.PIPE)
365
    AssertEqual(p.wait(), 0)
366

    
367
    # Return temporary filename
368
    return p.stdout.read().strip()
369
  finally:
370
    f.close()
371

    
372

    
373
def UploadData(node, data, mode=0600, filename=None):
374
  """Uploads data to a node and returns the filename.
375

376
  Caller needs to remove the returned file on the node when it's not needed
377
  anymore.
378

379
  """
380
  if filename:
381
    tmp = "tmp=%s" % utils.ShellQuote(filename)
382
  else:
383
    tmp = "tmp=$(tempfile --mode %o --prefix gnt)" % mode
384
  cmd = ("%s && "
385
         "[[ -f \"${tmp}\" ]] && "
386
         "cat > \"${tmp}\" && "
387
         "echo \"${tmp}\"") % tmp
388

    
389
  p = subprocess.Popen(GetSSHCommand(node, cmd), shell=False,
390
                       stdin=subprocess.PIPE, stdout=subprocess.PIPE)
391
  p.stdin.write(data)
392
  p.stdin.close()
393
  AssertEqual(p.wait(), 0)
394

    
395
  # Return temporary filename
396
  return p.stdout.read().strip()
397

    
398

    
399
def BackupFile(node, path):
400
  """Creates a backup of a file on the node and returns the filename.
401

402
  Caller needs to remove the returned file on the node when it's not needed
403
  anymore.
404

405
  """
406
  vpath = MakeNodePath(node, path)
407

    
408
  cmd = ("tmp=$(tempfile --prefix .gnt --directory=$(dirname %s)) && "
409
         "[[ -f \"$tmp\" ]] && "
410
         "cp %s $tmp && "
411
         "echo $tmp") % (utils.ShellQuote(vpath), utils.ShellQuote(vpath))
412

    
413
  # Return temporary filename
414
  result = GetCommandOutput(node, cmd).strip()
415

    
416
  print "Backup filename: %s" % result
417

    
418
  return result
419

    
420

    
421
def _ResolveName(cmd, key):
422
  """Helper function.
423

424
  """
425
  master = qa_config.GetMasterNode()
426

    
427
  output = GetCommandOutput(master.primary, utils.ShellQuoteArgs(cmd))
428
  for line in output.splitlines():
429
    (lkey, lvalue) = line.split(":", 1)
430
    if lkey == key:
431
      return lvalue.lstrip()
432
  raise KeyError("Key not found")
433

    
434

    
435
def ResolveInstanceName(instance):
436
  """Gets the full name of an instance.
437

438
  @type instance: string
439
  @param instance: Instance name
440

441
  """
442
  return _ResolveName(["gnt-instance", "info", instance],
443
                      "Instance name")
444

    
445

    
446
def ResolveNodeName(node):
447
  """Gets the full name of a node.
448

449
  """
450
  return _ResolveName(["gnt-node", "info", node.primary],
451
                      "Node name")
452

    
453

    
454
def GetNodeInstances(node, secondaries=False):
455
  """Gets a list of instances on a node.
456

457
  """
458
  master = qa_config.GetMasterNode()
459
  node_name = ResolveNodeName(node)
460

    
461
  # Get list of all instances
462
  cmd = ["gnt-instance", "list", "--separator=:", "--no-headers",
463
         "--output=name,pnode,snodes"]
464
  output = GetCommandOutput(master.primary, utils.ShellQuoteArgs(cmd))
465

    
466
  instances = []
467
  for line in output.splitlines():
468
    (name, pnode, snodes) = line.split(":", 2)
469
    if ((not secondaries and pnode == node_name) or
470
        (secondaries and node_name in snodes.split(","))):
471
      instances.append(name)
472

    
473
  return instances
474

    
475

    
476
def _SelectQueryFields(rnd, fields):
477
  """Generates a list of fields for query tests.
478

479
  """
480
  # Create copy for shuffling
481
  fields = list(fields)
482
  rnd.shuffle(fields)
483

    
484
  # Check all fields
485
  yield fields
486
  yield sorted(fields)
487

    
488
  # Duplicate fields
489
  yield fields + fields
490

    
491
  # Check small groups of fields
492
  while fields:
493
    yield [fields.pop() for _ in range(rnd.randint(2, 10)) if fields]
494

    
495

    
496
def _List(listcmd, fields, names):
497
  """Runs a list command.
498

499
  """
500
  master = qa_config.GetMasterNode()
501

    
502
  cmd = [listcmd, "list", "--separator=|", "--no-headers",
503
         "--output", ",".join(fields)]
504

    
505
  if names:
506
    cmd.extend(names)
507

    
508
  return GetCommandOutput(master.primary,
509
                          utils.ShellQuoteArgs(cmd)).splitlines()
510

    
511

    
512
def GenericQueryTest(cmd, fields, namefield="name", test_unknown=True):
513
  """Runs a number of tests on query commands.
514

515
  @param cmd: Command name
516
  @param fields: List of field names
517

518
  """
519
  rnd = random.Random(hash(cmd))
520

    
521
  fields = list(fields)
522
  rnd.shuffle(fields)
523

    
524
  # Test a number of field combinations
525
  for testfields in _SelectQueryFields(rnd, fields):
526
    AssertRedirectedCommand([cmd, "list", "--output", ",".join(testfields)])
527

    
528
  if namefield is not None:
529
    namelist_fn = compat.partial(_List, cmd, [namefield])
530

    
531
    # When no names were requested, the list must be sorted
532
    names = namelist_fn(None)
533
    AssertEqual(names, utils.NiceSort(names))
534

    
535
    # When requesting specific names, the order must be kept
536
    revnames = list(reversed(names))
537
    AssertEqual(namelist_fn(revnames), revnames)
538

    
539
    randnames = list(names)
540
    rnd.shuffle(randnames)
541
    AssertEqual(namelist_fn(randnames), randnames)
542

    
543
  if test_unknown:
544
    # Listing unknown items must fail
545
    AssertCommand([cmd, "list", "this.name.certainly.does.not.exist"],
546
                  fail=True)
547

    
548
  # Check exit code for listing unknown field
549
  AssertEqual(AssertRedirectedCommand([cmd, "list",
550
                                       "--output=field/does/not/exist"],
551
                                      fail=True),
552
              constants.EXIT_UNKNOWN_FIELD)
553

    
554

    
555
def GenericQueryFieldsTest(cmd, fields):
556
  master = qa_config.GetMasterNode()
557

    
558
  # Listing fields
559
  AssertRedirectedCommand([cmd, "list-fields"])
560
  AssertRedirectedCommand([cmd, "list-fields"] + fields)
561

    
562
  # Check listed fields (all, must be sorted)
563
  realcmd = [cmd, "list-fields", "--separator=|", "--no-headers"]
564
  output = GetCommandOutput(master.primary,
565
                            utils.ShellQuoteArgs(realcmd)).splitlines()
566
  AssertEqual([line.split("|", 1)[0] for line in output],
567
              utils.NiceSort(fields))
568

    
569
  # Check exit code for listing unknown field
570
  AssertEqual(AssertCommand([cmd, "list-fields", "field/does/not/exist"],
571
                            fail=True),
572
              constants.EXIT_UNKNOWN_FIELD)
573

    
574

    
575
def _FormatWithColor(text, seq):
576
  if not seq:
577
    return text
578
  return "%s%s%s" % (seq, text, _RESET_SEQ)
579

    
580

    
581
FormatWarning = lambda text: _FormatWithColor(text, _WARNING_SEQ)
582
FormatError = lambda text: _FormatWithColor(text, _ERROR_SEQ)
583
FormatInfo = lambda text: _FormatWithColor(text, _INFO_SEQ)
584

    
585

    
586
def AddToEtcHosts(hostnames):
587
  """Adds hostnames to /etc/hosts.
588

589
  @param hostnames: List of hostnames first used A records, all other CNAMEs
590

591
  """
592
  master = qa_config.GetMasterNode()
593
  tmp_hosts = UploadData(master.primary, "", mode=0644)
594

    
595
  data = []
596
  for localhost in ("::1", "127.0.0.1"):
597
    data.append("%s %s" % (localhost, " ".join(hostnames)))
598

    
599
  try:
600
    AssertCommand("{ cat %s && echo -e '%s'; } > %s && mv %s %s" %
601
                  (utils.ShellQuote(pathutils.ETC_HOSTS),
602
                   "\\n".join(data),
603
                   utils.ShellQuote(tmp_hosts),
604
                   utils.ShellQuote(tmp_hosts),
605
                   utils.ShellQuote(pathutils.ETC_HOSTS)))
606
  except Exception:
607
    AssertCommand(["rm", "-f", tmp_hosts])
608
    raise
609

    
610

    
611
def RemoveFromEtcHosts(hostnames):
612
  """Remove hostnames from /etc/hosts.
613

614
  @param hostnames: List of hostnames first used A records, all other CNAMEs
615

616
  """
617
  master = qa_config.GetMasterNode()
618
  tmp_hosts = UploadData(master.primary, "", mode=0644)
619
  quoted_tmp_hosts = utils.ShellQuote(tmp_hosts)
620

    
621
  sed_data = " ".join(hostnames)
622
  try:
623
    AssertCommand(("sed -e '/^\(::1\|127\.0\.0\.1\)\s\+%s/d' %s > %s"
624
                   " && mv %s %s") %
625
                   (sed_data, utils.ShellQuote(pathutils.ETC_HOSTS),
626
                    quoted_tmp_hosts, quoted_tmp_hosts,
627
                    utils.ShellQuote(pathutils.ETC_HOSTS)))
628
  except Exception:
629
    AssertCommand(["rm", "-f", tmp_hosts])
630
    raise
631

    
632

    
633
def RunInstanceCheck(instance, running):
634
  """Check if instance is running or not.
635

636
  """
637
  instance_name = _GetName(instance, operator.attrgetter("name"))
638

    
639
  script = qa_config.GetInstanceCheckScript()
640
  if not script:
641
    return
642

    
643
  master_node = qa_config.GetMasterNode()
644

    
645
  # Build command to connect to master node
646
  master_ssh = GetSSHCommand(master_node.primary, "--")
647

    
648
  if running:
649
    running_shellval = "1"
650
    running_text = ""
651
  else:
652
    running_shellval = ""
653
    running_text = "not "
654

    
655
  print FormatInfo("Checking if instance '%s' is %srunning" %
656
                   (instance_name, running_text))
657

    
658
  args = [script, instance_name]
659
  env = {
660
    "PATH": constants.HOOKS_PATH,
661
    "RUN_UUID": _RUN_UUID,
662
    "MASTER_SSH": utils.ShellQuoteArgs(master_ssh),
663
    "INSTANCE_NAME": instance_name,
664
    "INSTANCE_RUNNING": running_shellval,
665
    }
666

    
667
  result = os.spawnve(os.P_WAIT, script, args, env)
668
  if result != 0:
669
    raise qa_error.Error("Instance check failed with result %s" % result)
670

    
671

    
672
def _InstanceCheckInner(expected, instarg, args, result):
673
  """Helper function used by L{InstanceCheck}.
674

675
  """
676
  if instarg == FIRST_ARG:
677
    instance = args[0]
678
  elif instarg == RETURN_VALUE:
679
    instance = result
680
  else:
681
    raise Exception("Invalid value '%s' for instance argument" % instarg)
682

    
683
  if expected in (INST_DOWN, INST_UP):
684
    RunInstanceCheck(instance, (expected == INST_UP))
685
  elif expected is not None:
686
    raise Exception("Invalid value '%s'" % expected)
687

    
688

    
689
def InstanceCheck(before, after, instarg):
690
  """Decorator to check instance status before and after test.
691

692
  @param before: L{INST_DOWN} if instance must be stopped before test,
693
    L{INST_UP} if instance must be running before test, L{None} to not check.
694
  @param after: L{INST_DOWN} if instance must be stopped after test,
695
    L{INST_UP} if instance must be running after test, L{None} to not check.
696
  @param instarg: L{FIRST_ARG} to use first argument to test as instance (a
697
    dictionary), L{RETURN_VALUE} to use return value (disallows pre-checks)
698

699
  """
700
  def decorator(fn):
701
    @functools.wraps(fn)
702
    def wrapper(*args, **kwargs):
703
      _InstanceCheckInner(before, instarg, args, NotImplemented)
704

    
705
      result = fn(*args, **kwargs)
706

    
707
      _InstanceCheckInner(after, instarg, args, result)
708

    
709
      return result
710
    return wrapper
711
  return decorator
712

    
713

    
714
def GetNonexistentGroups(count):
715
  """Gets group names which shouldn't exist on the cluster.
716

717
  @param count: Number of groups to get
718
  @rtype: integer
719

720
  """
721
  return GetNonexistentEntityNames(count, "groups", "group")
722

    
723

    
724
def GetNonexistentEntityNames(count, name_config, name_prefix):
725
  """Gets entity names which shouldn't exist on the cluster.
726

727
  The actualy names can refer to arbitrary entities (for example
728
  groups, networks).
729

730
  @param count: Number of names to get
731
  @rtype: integer
732
  @param name_config: name of the leaf in the config containing
733
    this entity's configuration, including a 'inexistent-'
734
    element
735
  @rtype: string
736
  @param name_prefix: prefix of the entity's names, used to compose
737
    the default values; for example for groups, the prefix is
738
    'group' and the generated names are then group1, group2, ...
739
  @rtype: string
740

741
  """
742
  entities = qa_config.get(name_config, {})
743

    
744
  default = [name_prefix + str(i) for i in range(count)]
745
  assert count <= len(default)
746

    
747
  name_config_inexistent = "inexistent-" + name_config
748
  candidates = entities.get(name_config_inexistent, default)[:count]
749

    
750
  if len(candidates) < count:
751
    raise Exception("At least %s non-existent %s are needed" %
752
                    (count, name_config))
753

    
754
  return candidates
755

    
756

    
757
def MakeNodePath(node, path):
758
  """Builds an absolute path for a virtual node.
759

760
  @type node: string or L{qa_config._QaNode}
761
  @param node: Node
762
  @type path: string
763
  @param path: Path without node-specific prefix
764

765
  """
766
  (_, basedir) = qa_config.GetVclusterSettings()
767

    
768
  if isinstance(node, basestring):
769
    name = node
770
  else:
771
    name = node.primary
772

    
773
  if basedir:
774
    assert path.startswith("/")
775
    return "%s%s" % (vcluster.MakeNodeRoot(basedir, name), path)
776
  else:
777
    return path