Statistics
| Branch: | Tag: | Revision:

root / qa / qa_utils.py @ 63e08b25

History | View | Annotate | Download (24.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 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 ResolveInstanceName(instance):
438
  """Gets the full name of an instance.
439

440
  @type instance: string
441
  @param instance: Instance name
442

443
  """
444
  info = GetObjectInfo(["gnt-instance", "info", instance])
445
  return info[0]["Instance name"]
446

    
447

    
448
def ResolveNodeName(node):
449
  """Gets the full name of a node.
450

451
  """
452
  info = GetObjectInfo(["gnt-node", "info", node.primary])
453
  return info[0]["Node name"]
454

    
455

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

459
  """
460
  master = qa_config.GetMasterNode()
461
  node_name = ResolveNodeName(node)
462

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

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

    
475
  return instances
476

    
477

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

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

    
486
  # Check all fields
487
  yield fields
488
  yield sorted(fields)
489

    
490
  # Duplicate fields
491
  yield fields + fields
492

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

    
497

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

501
  """
502
  master = qa_config.GetMasterNode()
503

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

    
507
  if names:
508
    cmd.extend(names)
509

    
510
  return GetCommandOutput(master.primary,
511
                          utils.ShellQuoteArgs(cmd)).splitlines()
512

    
513

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

517
  @param cmd: Command name
518
  @param fields: List of field names
519

520
  """
521
  rnd = random.Random(hash(cmd))
522

    
523
  fields = list(fields)
524
  rnd.shuffle(fields)
525

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

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

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

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

    
541
    randnames = list(names)
542
    rnd.shuffle(randnames)
543
    AssertEqual(namelist_fn(randnames), randnames)
544

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

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

    
556

    
557
def GenericQueryFieldsTest(cmd, fields):
558
  master = qa_config.GetMasterNode()
559

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

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

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

    
576

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

    
582

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

    
587

    
588
def AddToEtcHosts(hostnames):
589
  """Adds hostnames to /etc/hosts.
590

591
  @param hostnames: List of hostnames first used A records, all other CNAMEs
592

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

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

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

    
612

    
613
def RemoveFromEtcHosts(hostnames):
614
  """Remove hostnames from /etc/hosts.
615

616
  @param hostnames: List of hostnames first used A records, all other CNAMEs
617

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

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

    
634

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

638
  """
639
  instance_name = _GetName(instance, operator.attrgetter("name"))
640

    
641
  script = qa_config.GetInstanceCheckScript()
642
  if not script:
643
    return
644

    
645
  master_node = qa_config.GetMasterNode()
646

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

    
650
  if running:
651
    running_shellval = "1"
652
    running_text = ""
653
  else:
654
    running_shellval = ""
655
    running_text = "not "
656

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

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

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

    
673

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

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

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

    
690

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

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

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

    
707
      result = fn(*args, **kwargs)
708

    
709
      _InstanceCheckInner(after, instarg, args, result)
710

    
711
      return result
712
    return wrapper
713
  return decorator
714

    
715

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

719
  @param count: Number of groups to get
720
  @rtype: integer
721

722
  """
723
  return GetNonexistentEntityNames(count, "groups", "group")
724

    
725

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

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

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

743
  """
744
  entities = qa_config.get(name_config, {})
745

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

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

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

    
756
  return candidates
757

    
758

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

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

767
  """
768
  (_, basedir) = qa_config.GetVclusterSettings()
769

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

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

    
781

    
782
def _GetParameterOptions(key, specs, old_specs):
783
  """Helper to build policy options."""
784
  values = ["%s=%s" % (par, keyvals[key])
785
            for (par, keyvals) in specs.items()
786
            if key in keyvals]
787
  if old_specs:
788
    present_pars = frozenset(par
789
                             for (par, keyvals) in specs.items()
790
                             if key in keyvals)
791
    values.extend("%s=%s" % (par, keyvals[key])
792
                  for (par, keyvals) in old_specs.items()
793
                  if key in keyvals and par not in present_pars)
794
  return ",".join(values)
795

    
796

    
797
def TestSetISpecs(new_specs, get_policy_fn=None, build_cmd_fn=None,
798
                  fail=False, old_values=None):
799
  """Change instance specs for an object.
800

801
  @type new_specs: dict of dict
802
  @param new_specs: new_specs[par][key], where key is "min", "max", "std". It
803
      can be an empty dictionary.
804
  @type get_policy_fn: function
805
  @param get_policy_fn: function that returns the current policy as in
806
      L{qa_cluster._GetClusterIPolicy}
807
  @type build_cmd_fn: function
808
  @param build_cmd_fn: function that return the full command line from the
809
      options alone
810
  @type fail: bool
811
  @param fail: if the change is expected to fail
812
  @type old_values: tuple
813
  @param old_values: (old_policy, old_specs), as returned by
814
     L{qa_cluster._GetClusterIPolicy}
815
  @return: same as L{qa_cluster._GetClusterIPolicy}
816

817
  """
818
  assert get_policy_fn is not None
819
  assert build_cmd_fn is not None
820

    
821
  if old_values:
822
    (old_policy, old_specs) = old_values
823
  else:
824
    (old_policy, old_specs) = get_policy_fn()
825
  if new_specs:
826
    cmd = []
827
    if any(("min" in val or "max" in val) for val in new_specs.values()):
828
      minmax_opt_items = []
829
      for key in ["min", "max"]:
830
        keyopt = _GetParameterOptions(key, new_specs, old_specs)
831
        minmax_opt_items.append("%s:%s" % (key, keyopt))
832
      cmd.extend([
833
        "--ipolicy-bounds-specs",
834
        "/".join(minmax_opt_items)
835
        ])
836
    std_opt = _GetParameterOptions("std", new_specs, {})
837
    if std_opt:
838
      cmd.extend(["--ipolicy-std-specs", std_opt])
839
    AssertCommand(build_cmd_fn(cmd), fail=fail)
840

    
841
  # Check the new state
842
  (eff_policy, eff_specs) = get_policy_fn()
843
  AssertEqual(eff_policy, old_policy)
844
  if fail:
845
    AssertEqual(eff_specs, old_specs)
846
  else:
847
    for par in eff_specs:
848
      for key in eff_specs[par]:
849
        if par in new_specs and key in new_specs[par]:
850
          AssertEqual(int(eff_specs[par][key]), int(new_specs[par][key]))
851
        else:
852
          AssertEqual(int(eff_specs[par][key]), int(old_specs[par][key]))
853
  return (eff_policy, eff_specs)
854

    
855

    
856
def ParseIPolicy(policy):
857
  """Parse and split instance an instance policy.
858

859
  @type policy: dict
860
  @param policy: policy, as returned by L{GetObjectInfo}
861
  @rtype: tuple
862
  @return: (policy, specs), where:
863
      - policy is a dictionary of the policy values, instance specs excluded
864
      - specs is dict of dict, specs[par][key] is a spec value, where key is
865
        "min", "max", or "std"
866

867
  """
868
  ret_specs = {}
869
  ret_policy = {}
870
  ispec_keys = constants.ISPECS_MINMAX_KEYS | frozenset([constants.ISPECS_STD])
871
  for (key, val) in policy.items():
872
    if key in ispec_keys:
873
      for (par, pval) in val.items():
874
        d = ret_specs.setdefault(par, {})
875
        d[key] = pval
876
    else:
877
      ret_policy[key] = val
878
  return (ret_policy, ret_specs)