Statistics
| Branch: | Tag: | Revision:

root / qa / qa_utils.py @ 50eaa5da

History | View | Annotate | Download (20.2 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
  cmd = ("tmp=$(tempfile --prefix .gnt --directory=$(dirname %s)) && "
407
         "[[ -f \"$tmp\" ]] && "
408
         "cp %s $tmp && "
409
         "echo $tmp") % (utils.ShellQuote(path), utils.ShellQuote(path))
410

    
411
  # Return temporary filename
412
  return GetCommandOutput(node, cmd).strip()
413

    
414

    
415
def _ResolveName(cmd, key):
416
  """Helper function.
417

418
  """
419
  master = qa_config.GetMasterNode()
420

    
421
  output = GetCommandOutput(master.primary, utils.ShellQuoteArgs(cmd))
422
  for line in output.splitlines():
423
    (lkey, lvalue) = line.split(":", 1)
424
    if lkey == key:
425
      return lvalue.lstrip()
426
  raise KeyError("Key not found")
427

    
428

    
429
def ResolveInstanceName(instance):
430
  """Gets the full name of an instance.
431

432
  @type instance: string
433
  @param instance: Instance name
434

435
  """
436
  return _ResolveName(["gnt-instance", "info", instance],
437
                      "Instance name")
438

    
439

    
440
def ResolveNodeName(node):
441
  """Gets the full name of a node.
442

443
  """
444
  return _ResolveName(["gnt-node", "info", node.primary],
445
                      "Node name")
446

    
447

    
448
def GetNodeInstances(node, secondaries=False):
449
  """Gets a list of instances on a node.
450

451
  """
452
  master = qa_config.GetMasterNode()
453
  node_name = ResolveNodeName(node)
454

    
455
  # Get list of all instances
456
  cmd = ["gnt-instance", "list", "--separator=:", "--no-headers",
457
         "--output=name,pnode,snodes"]
458
  output = GetCommandOutput(master.primary, utils.ShellQuoteArgs(cmd))
459

    
460
  instances = []
461
  for line in output.splitlines():
462
    (name, pnode, snodes) = line.split(":", 2)
463
    if ((not secondaries and pnode == node_name) or
464
        (secondaries and node_name in snodes.split(","))):
465
      instances.append(name)
466

    
467
  return instances
468

    
469

    
470
def _SelectQueryFields(rnd, fields):
471
  """Generates a list of fields for query tests.
472

473
  """
474
  # Create copy for shuffling
475
  fields = list(fields)
476
  rnd.shuffle(fields)
477

    
478
  # Check all fields
479
  yield fields
480
  yield sorted(fields)
481

    
482
  # Duplicate fields
483
  yield fields + fields
484

    
485
  # Check small groups of fields
486
  while fields:
487
    yield [fields.pop() for _ in range(rnd.randint(2, 10)) if fields]
488

    
489

    
490
def _List(listcmd, fields, names):
491
  """Runs a list command.
492

493
  """
494
  master = qa_config.GetMasterNode()
495

    
496
  cmd = [listcmd, "list", "--separator=|", "--no-headers",
497
         "--output", ",".join(fields)]
498

    
499
  if names:
500
    cmd.extend(names)
501

    
502
  return GetCommandOutput(master.primary,
503
                          utils.ShellQuoteArgs(cmd)).splitlines()
504

    
505

    
506
def GenericQueryTest(cmd, fields, namefield="name", test_unknown=True):
507
  """Runs a number of tests on query commands.
508

509
  @param cmd: Command name
510
  @param fields: List of field names
511

512
  """
513
  rnd = random.Random(hash(cmd))
514

    
515
  fields = list(fields)
516
  rnd.shuffle(fields)
517

    
518
  # Test a number of field combinations
519
  for testfields in _SelectQueryFields(rnd, fields):
520
    AssertRedirectedCommand([cmd, "list", "--output", ",".join(testfields)])
521

    
522
  if namefield is not None:
523
    namelist_fn = compat.partial(_List, cmd, [namefield])
524

    
525
    # When no names were requested, the list must be sorted
526
    names = namelist_fn(None)
527
    AssertEqual(names, utils.NiceSort(names))
528

    
529
    # When requesting specific names, the order must be kept
530
    revnames = list(reversed(names))
531
    AssertEqual(namelist_fn(revnames), revnames)
532

    
533
    randnames = list(names)
534
    rnd.shuffle(randnames)
535
    AssertEqual(namelist_fn(randnames), randnames)
536

    
537
  if test_unknown:
538
    # Listing unknown items must fail
539
    AssertCommand([cmd, "list", "this.name.certainly.does.not.exist"],
540
                  fail=True)
541

    
542
  # Check exit code for listing unknown field
543
  AssertEqual(AssertRedirectedCommand([cmd, "list",
544
                                       "--output=field/does/not/exist"],
545
                                      fail=True),
546
              constants.EXIT_UNKNOWN_FIELD)
547

    
548

    
549
def GenericQueryFieldsTest(cmd, fields):
550
  master = qa_config.GetMasterNode()
551

    
552
  # Listing fields
553
  AssertRedirectedCommand([cmd, "list-fields"])
554
  AssertRedirectedCommand([cmd, "list-fields"] + fields)
555

    
556
  # Check listed fields (all, must be sorted)
557
  realcmd = [cmd, "list-fields", "--separator=|", "--no-headers"]
558
  output = GetCommandOutput(master.primary,
559
                            utils.ShellQuoteArgs(realcmd)).splitlines()
560
  AssertEqual([line.split("|", 1)[0] for line in output],
561
              utils.NiceSort(fields))
562

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

    
568

    
569
def _FormatWithColor(text, seq):
570
  if not seq:
571
    return text
572
  return "%s%s%s" % (seq, text, _RESET_SEQ)
573

    
574

    
575
FormatWarning = lambda text: _FormatWithColor(text, _WARNING_SEQ)
576
FormatError = lambda text: _FormatWithColor(text, _ERROR_SEQ)
577
FormatInfo = lambda text: _FormatWithColor(text, _INFO_SEQ)
578

    
579

    
580
def AddToEtcHosts(hostnames):
581
  """Adds hostnames to /etc/hosts.
582

583
  @param hostnames: List of hostnames first used A records, all other CNAMEs
584

585
  """
586
  master = qa_config.GetMasterNode()
587
  tmp_hosts = UploadData(master.primary, "", mode=0644)
588

    
589
  data = []
590
  for localhost in ("::1", "127.0.0.1"):
591
    data.append("%s %s" % (localhost, " ".join(hostnames)))
592

    
593
  try:
594
    AssertCommand("{ cat %s && echo -e '%s'; } > %s && mv %s %s" %
595
                  (utils.ShellQuote(pathutils.ETC_HOSTS),
596
                   "\\n".join(data),
597
                   utils.ShellQuote(tmp_hosts),
598
                   utils.ShellQuote(tmp_hosts),
599
                   utils.ShellQuote(pathutils.ETC_HOSTS)))
600
  except Exception:
601
    AssertCommand(["rm", "-f", tmp_hosts])
602
    raise
603

    
604

    
605
def RemoveFromEtcHosts(hostnames):
606
  """Remove hostnames from /etc/hosts.
607

608
  @param hostnames: List of hostnames first used A records, all other CNAMEs
609

610
  """
611
  master = qa_config.GetMasterNode()
612
  tmp_hosts = UploadData(master.primary, "", mode=0644)
613
  quoted_tmp_hosts = utils.ShellQuote(tmp_hosts)
614

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

    
626

    
627
def RunInstanceCheck(instance, running):
628
  """Check if instance is running or not.
629

630
  """
631
  instance_name = _GetName(instance, operator.attrgetter("name"))
632

    
633
  script = qa_config.GetInstanceCheckScript()
634
  if not script:
635
    return
636

    
637
  master_node = qa_config.GetMasterNode()
638

    
639
  # Build command to connect to master node
640
  master_ssh = GetSSHCommand(master_node.primary, "--")
641

    
642
  if running:
643
    running_shellval = "1"
644
    running_text = ""
645
  else:
646
    running_shellval = ""
647
    running_text = "not "
648

    
649
  print FormatInfo("Checking if instance '%s' is %srunning" %
650
                   (instance_name, running_text))
651

    
652
  args = [script, instance_name]
653
  env = {
654
    "PATH": constants.HOOKS_PATH,
655
    "RUN_UUID": _RUN_UUID,
656
    "MASTER_SSH": utils.ShellQuoteArgs(master_ssh),
657
    "INSTANCE_NAME": instance_name,
658
    "INSTANCE_RUNNING": running_shellval,
659
    }
660

    
661
  result = os.spawnve(os.P_WAIT, script, args, env)
662
  if result != 0:
663
    raise qa_error.Error("Instance check failed with result %s" % result)
664

    
665

    
666
def _InstanceCheckInner(expected, instarg, args, result):
667
  """Helper function used by L{InstanceCheck}.
668

669
  """
670
  if instarg == FIRST_ARG:
671
    instance = args[0]
672
  elif instarg == RETURN_VALUE:
673
    instance = result
674
  else:
675
    raise Exception("Invalid value '%s' for instance argument" % instarg)
676

    
677
  if expected in (INST_DOWN, INST_UP):
678
    RunInstanceCheck(instance, (expected == INST_UP))
679
  elif expected is not None:
680
    raise Exception("Invalid value '%s'" % expected)
681

    
682

    
683
def InstanceCheck(before, after, instarg):
684
  """Decorator to check instance status before and after test.
685

686
  @param before: L{INST_DOWN} if instance must be stopped before test,
687
    L{INST_UP} if instance must be running before test, L{None} to not check.
688
  @param after: L{INST_DOWN} if instance must be stopped after test,
689
    L{INST_UP} if instance must be running after test, L{None} to not check.
690
  @param instarg: L{FIRST_ARG} to use first argument to test as instance (a
691
    dictionary), L{RETURN_VALUE} to use return value (disallows pre-checks)
692

693
  """
694
  def decorator(fn):
695
    @functools.wraps(fn)
696
    def wrapper(*args, **kwargs):
697
      _InstanceCheckInner(before, instarg, args, NotImplemented)
698

    
699
      result = fn(*args, **kwargs)
700

    
701
      _InstanceCheckInner(after, instarg, args, result)
702

    
703
      return result
704
    return wrapper
705
  return decorator
706

    
707

    
708
def GetNonexistentGroups(count):
709
  """Gets group names which shouldn't exist on the cluster.
710

711
  @param count: Number of groups to get
712
  @rtype: integer
713

714
  """
715
  return GetNonexistentEntityNames(count, "groups", "group")
716

    
717

    
718
def GetNonexistentEntityNames(count, name_config, name_prefix):
719
  """Gets entity names which shouldn't exist on the cluster.
720

721
  The actualy names can refer to arbitrary entities (for example
722
  groups, networks).
723

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

735
  """
736
  entities = qa_config.get(name_config, {})
737

    
738
  default = [name_prefix + str(i) for i in range(count)]
739
  assert count <= len(default)
740

    
741
  name_config_inexistent = "inexistent-" + name_config
742
  candidates = entities.get(name_config_inexistent, default)[:count]
743

    
744
  if len(candidates) < count:
745
    raise Exception("At least %s non-existent %s are needed" %
746
                    (count, name_config))
747

    
748
  return candidates