Statistics
| Branch: | Tag: | Revision:

root / qa / qa_utils.py @ 1490a90c

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

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

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

    
48
import colors
49
import qa_config
50
import qa_error
51

    
52

    
53
_INFO_SEQ = None
54
_WARNING_SEQ = None
55
_ERROR_SEQ = None
56
_RESET_SEQ = None
57

    
58
_MULTIPLEXERS = {}
59

    
60
#: Unique ID per QA run
61
_RUN_UUID = utils.NewUUID()
62

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

    
66

    
67
(INST_DOWN,
68
 INST_UP) = range(500, 502)
69

    
70
(FIRST_ARG,
71
 RETURN_VALUE) = range(1000, 1002)
72

    
73

    
74
def _SetupColours():
75
  """Initializes the colour constants.
76

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

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

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

    
92
  curses.setupterm()
93

    
94
  _RESET_SEQ = curses.tigetstr("op")
95

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

    
101

    
102
_SetupColours()
103

    
104

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

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

    
112

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

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

    
120

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

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

    
128

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

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

    
136

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

140
  @type entity: string or dict
141
  @param fn: Function retrieving name from entity
142

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

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

    
152
  return result
153

    
154

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

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

    
166

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

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

182
  """
183
  if node is None:
184
    node = qa_config.GetMasterNode()
185

    
186
  nodename = _GetName(node, operator.attrgetter("primary"))
187

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

    
193
  rcode = StartSSH(nodename, cmdstr, log_cmd=log_cmd).wait()
194
  _AssertRetCode(rcode, fail, cmdstr, nodename)
195

    
196
  return rcode
197

    
198

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

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

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

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

    
219

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

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

235
  """
236
  args = ["ssh", "-oEscapeChar=none", "-oBatchMode=yes", "-lroot"]
237

    
238
  if tty is None:
239
    tty = sys.stdout.isatty()
240

    
241
  if tty:
242
    args.append("-t")
243

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

    
258
  (vcluster_master, vcluster_basedir) = \
259
    qa_config.GetVclusterSettings()
260

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

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

    
274
    if cmd:
275
      args.append(cmd)
276

    
277
  return args
278

    
279

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

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

    
293

    
294
def StartSSH(node, cmd, strict=True, log_cmd=True):
295
  """Starts SSH.
296

297
  """
298
  return StartLocalCommand(GetSSHCommand(node, cmd, strict=strict),
299
                           _nolog_opts=True, log_cmd=log_cmd)
300

    
301

    
302
def StartMultiplexer(node):
303
  """Starts a multiplexer command.
304

305
  @param node: the node for which to open the multiplexer
306

307
  """
308
  if node in _MULTIPLEXERS:
309
    return
310

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

    
319

    
320
def CloseMultiplexers():
321
  """Closes all current multiplexers and cleans up.
322

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

    
329

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

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

    
349

    
350
def GetObjectInfo(infocmd):
351
  """Get and parse information about a Ganeti object.
352

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

358
  """
359
  master = qa_config.GetMasterNode()
360
  cmdline = utils.ShellQuoteArgs(infocmd)
361
  info_out = GetCommandOutput(master.primary, cmdline)
362
  return yaml.load(info_out)
363

    
364

    
365
def UploadFile(node, src):
366
  """Uploads a file to a node and returns the filename.
367

368
  Caller needs to remove the returned file on the node when it's not needed
369
  anymore.
370

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

    
375
  cmd = ('tmp=$(mktemp --tmpdir gnt.XXXXXX) && '
376
         'chmod %o "${tmp}" && '
377
         '[[ -f "${tmp}" ]] && '
378
         'cat > "${tmp}" && '
379
         'echo "${tmp}"') % mode
380

    
381
  f = open(src, "r")
382
  try:
383
    p = subprocess.Popen(GetSSHCommand(node, cmd), shell=False, stdin=f,
384
                         stdout=subprocess.PIPE)
385
    AssertEqual(p.wait(), 0)
386

    
387
    # Return temporary filename
388
    return p.stdout.read().strip()
389
  finally:
390
    f.close()
391

    
392

    
393
def UploadData(node, data, mode=0600, filename=None):
394
  """Uploads data to a node and returns the filename.
395

396
  Caller needs to remove the returned file on the node when it's not needed
397
  anymore.
398

399
  """
400
  if filename:
401
    tmp = "tmp=%s" % utils.ShellQuote(filename)
402
  else:
403
    tmp = ('tmp=$(mktemp --tmpdir gnt.XXXXXX) && '
404
           'chmod %o "${tmp}"') % mode
405
  cmd = ("%s && "
406
         "[[ -f \"${tmp}\" ]] && "
407
         "cat > \"${tmp}\" && "
408
         "echo \"${tmp}\"") % tmp
409

    
410
  p = subprocess.Popen(GetSSHCommand(node, cmd), shell=False,
411
                       stdin=subprocess.PIPE, stdout=subprocess.PIPE)
412
  p.stdin.write(data)
413
  p.stdin.close()
414
  AssertEqual(p.wait(), 0)
415

    
416
  # Return temporary filename
417
  return p.stdout.read().strip()
418

    
419

    
420
def BackupFile(node, path):
421
  """Creates a backup of a file on the node and returns the filename.
422

423
  Caller needs to remove the returned file on the node when it's not needed
424
  anymore.
425

426
  """
427
  vpath = MakeNodePath(node, path)
428

    
429
  cmd = ("tmp=$(mktemp .gnt.XXXXXX --tmpdir=$(dirname %s)) && "
430
         "[[ -f \"$tmp\" ]] && "
431
         "cp %s $tmp && "
432
         "echo $tmp") % (utils.ShellQuote(vpath), utils.ShellQuote(vpath))
433

    
434
  # Return temporary filename
435
  result = GetCommandOutput(node, cmd).strip()
436

    
437
  print "Backup filename: %s" % result
438

    
439
  return result
440

    
441

    
442
def ResolveInstanceName(instance):
443
  """Gets the full name of an instance.
444

445
  @type instance: string
446
  @param instance: Instance name
447

448
  """
449
  info = GetObjectInfo(["gnt-instance", "info", instance])
450
  return info[0]["Instance name"]
451

    
452

    
453
def ResolveNodeName(node):
454
  """Gets the full name of a node.
455

456
  """
457
  info = GetObjectInfo(["gnt-node", "info", node.primary])
458
  return info[0]["Node name"]
459

    
460

    
461
def GetNodeInstances(node, secondaries=False):
462
  """Gets a list of instances on a node.
463

464
  """
465
  master = qa_config.GetMasterNode()
466
  node_name = ResolveNodeName(node)
467

    
468
  # Get list of all instances
469
  cmd = ["gnt-instance", "list", "--separator=:", "--no-headers",
470
         "--output=name,pnode,snodes"]
471
  output = GetCommandOutput(master.primary, utils.ShellQuoteArgs(cmd))
472

    
473
  instances = []
474
  for line in output.splitlines():
475
    (name, pnode, snodes) = line.split(":", 2)
476
    if ((not secondaries and pnode == node_name) or
477
        (secondaries and node_name in snodes.split(","))):
478
      instances.append(name)
479

    
480
  return instances
481

    
482

    
483
def _SelectQueryFields(rnd, fields):
484
  """Generates a list of fields for query tests.
485

486
  """
487
  # Create copy for shuffling
488
  fields = list(fields)
489
  rnd.shuffle(fields)
490

    
491
  # Check all fields
492
  yield fields
493
  yield sorted(fields)
494

    
495
  # Duplicate fields
496
  yield fields + fields
497

    
498
  # Check small groups of fields
499
  while fields:
500
    yield [fields.pop() for _ in range(rnd.randint(2, 10)) if fields]
501

    
502

    
503
def _List(listcmd, fields, names):
504
  """Runs a list command.
505

506
  """
507
  master = qa_config.GetMasterNode()
508

    
509
  cmd = [listcmd, "list", "--separator=|", "--no-headers",
510
         "--output", ",".join(fields)]
511

    
512
  if names:
513
    cmd.extend(names)
514

    
515
  return GetCommandOutput(master.primary,
516
                          utils.ShellQuoteArgs(cmd)).splitlines()
517

    
518

    
519
def GenericQueryTest(cmd, fields, namefield="name", test_unknown=True):
520
  """Runs a number of tests on query commands.
521

522
  @param cmd: Command name
523
  @param fields: List of field names
524

525
  """
526
  rnd = random.Random(hash(cmd))
527

    
528
  fields = list(fields)
529
  rnd.shuffle(fields)
530

    
531
  # Test a number of field combinations
532
  for testfields in _SelectQueryFields(rnd, fields):
533
    AssertRedirectedCommand([cmd, "list", "--output", ",".join(testfields)])
534

    
535
  if namefield is not None:
536
    namelist_fn = compat.partial(_List, cmd, [namefield])
537

    
538
    # When no names were requested, the list must be sorted
539
    names = namelist_fn(None)
540
    AssertEqual(names, utils.NiceSort(names))
541

    
542
    # When requesting specific names, the order must be kept
543
    revnames = list(reversed(names))
544
    AssertEqual(namelist_fn(revnames), revnames)
545

    
546
    randnames = list(names)
547
    rnd.shuffle(randnames)
548
    AssertEqual(namelist_fn(randnames), randnames)
549

    
550
  if test_unknown:
551
    # Listing unknown items must fail
552
    AssertCommand([cmd, "list", "this.name.certainly.does.not.exist"],
553
                  fail=True)
554

    
555
  # Check exit code for listing unknown field
556
  AssertEqual(AssertRedirectedCommand([cmd, "list",
557
                                       "--output=field/does/not/exist"],
558
                                      fail=True),
559
              constants.EXIT_UNKNOWN_FIELD)
560

    
561

    
562
def GenericQueryFieldsTest(cmd, fields):
563
  master = qa_config.GetMasterNode()
564

    
565
  # Listing fields
566
  AssertRedirectedCommand([cmd, "list-fields"])
567
  AssertRedirectedCommand([cmd, "list-fields"] + fields)
568

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

    
576
  # Check exit code for listing unknown field
577
  AssertEqual(AssertCommand([cmd, "list-fields", "field/does/not/exist"],
578
                            fail=True),
579
              constants.EXIT_UNKNOWN_FIELD)
580

    
581

    
582
def _FormatWithColor(text, seq):
583
  if not seq:
584
    return text
585
  return "%s%s%s" % (seq, text, _RESET_SEQ)
586

    
587

    
588
FormatWarning = lambda text: _FormatWithColor(text, _WARNING_SEQ)
589
FormatError = lambda text: _FormatWithColor(text, _ERROR_SEQ)
590
FormatInfo = lambda text: _FormatWithColor(text, _INFO_SEQ)
591

    
592

    
593
def AddToEtcHosts(hostnames):
594
  """Adds hostnames to /etc/hosts.
595

596
  @param hostnames: List of hostnames first used A records, all other CNAMEs
597

598
  """
599
  master = qa_config.GetMasterNode()
600
  tmp_hosts = UploadData(master.primary, "", mode=0644)
601

    
602
  data = []
603
  for localhost in ("::1", "127.0.0.1"):
604
    data.append("%s %s" % (localhost, " ".join(hostnames)))
605

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

    
617

    
618
def RemoveFromEtcHosts(hostnames):
619
  """Remove hostnames from /etc/hosts.
620

621
  @param hostnames: List of hostnames first used A records, all other CNAMEs
622

623
  """
624
  master = qa_config.GetMasterNode()
625
  tmp_hosts = UploadData(master.primary, "", mode=0644)
626
  quoted_tmp_hosts = utils.ShellQuote(tmp_hosts)
627

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

    
639

    
640
def RunInstanceCheck(instance, running):
641
  """Check if instance is running or not.
642

643
  """
644
  instance_name = _GetName(instance, operator.attrgetter("name"))
645

    
646
  script = qa_config.GetInstanceCheckScript()
647
  if not script:
648
    return
649

    
650
  master_node = qa_config.GetMasterNode()
651

    
652
  # Build command to connect to master node
653
  master_ssh = GetSSHCommand(master_node.primary, "--")
654

    
655
  if running:
656
    running_shellval = "1"
657
    running_text = ""
658
  else:
659
    running_shellval = ""
660
    running_text = "not "
661

    
662
  print FormatInfo("Checking if instance '%s' is %srunning" %
663
                   (instance_name, running_text))
664

    
665
  args = [script, instance_name]
666
  env = {
667
    "PATH": constants.HOOKS_PATH,
668
    "RUN_UUID": _RUN_UUID,
669
    "MASTER_SSH": utils.ShellQuoteArgs(master_ssh),
670
    "INSTANCE_NAME": instance_name,
671
    "INSTANCE_RUNNING": running_shellval,
672
    }
673

    
674
  result = os.spawnve(os.P_WAIT, script, args, env)
675
  if result != 0:
676
    raise qa_error.Error("Instance check failed with result %s" % result)
677

    
678

    
679
def _InstanceCheckInner(expected, instarg, args, result):
680
  """Helper function used by L{InstanceCheck}.
681

682
  """
683
  if instarg == FIRST_ARG:
684
    instance = args[0]
685
  elif instarg == RETURN_VALUE:
686
    instance = result
687
  else:
688
    raise Exception("Invalid value '%s' for instance argument" % instarg)
689

    
690
  if expected in (INST_DOWN, INST_UP):
691
    RunInstanceCheck(instance, (expected == INST_UP))
692
  elif expected is not None:
693
    raise Exception("Invalid value '%s'" % expected)
694

    
695

    
696
def InstanceCheck(before, after, instarg):
697
  """Decorator to check instance status before and after test.
698

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

706
  """
707
  def decorator(fn):
708
    @functools.wraps(fn)
709
    def wrapper(*args, **kwargs):
710
      _InstanceCheckInner(before, instarg, args, NotImplemented)
711

    
712
      result = fn(*args, **kwargs)
713

    
714
      _InstanceCheckInner(after, instarg, args, result)
715

    
716
      return result
717
    return wrapper
718
  return decorator
719

    
720

    
721
def GetNonexistentGroups(count):
722
  """Gets group names which shouldn't exist on the cluster.
723

724
  @param count: Number of groups to get
725
  @rtype: integer
726

727
  """
728
  return GetNonexistentEntityNames(count, "groups", "group")
729

    
730

    
731
def GetNonexistentEntityNames(count, name_config, name_prefix):
732
  """Gets entity names which shouldn't exist on the cluster.
733

734
  The actualy names can refer to arbitrary entities (for example
735
  groups, networks).
736

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

748
  """
749
  entities = qa_config.get(name_config, {})
750

    
751
  default = [name_prefix + str(i) for i in range(count)]
752
  assert count <= len(default)
753

    
754
  name_config_inexistent = "inexistent-" + name_config
755
  candidates = entities.get(name_config_inexistent, default)[:count]
756

    
757
  if len(candidates) < count:
758
    raise Exception("At least %s non-existent %s are needed" %
759
                    (count, name_config))
760

    
761
  return candidates
762

    
763

    
764
def MakeNodePath(node, path):
765
  """Builds an absolute path for a virtual node.
766

767
  @type node: string or L{qa_config._QaNode}
768
  @param node: Node
769
  @type path: string
770
  @param path: Path without node-specific prefix
771

772
  """
773
  (_, basedir) = qa_config.GetVclusterSettings()
774

    
775
  if isinstance(node, basestring):
776
    name = node
777
  else:
778
    name = node.primary
779

    
780
  if basedir:
781
    assert path.startswith("/")
782
    return "%s%s" % (vcluster.MakeNodeRoot(basedir, name), path)
783
  else:
784
    return path
785

    
786

    
787
def _GetParameterOptions(specs):
788
  """Helper to build policy options."""
789
  values = ["%s=%s" % (par, val)
790
            for (par, val) in specs.items()]
791
  return ",".join(values)
792

    
793

    
794
def TestSetISpecs(new_specs=None, diff_specs=None, get_policy_fn=None,
795
                  build_cmd_fn=None, fail=False, old_values=None):
796
  """Change instance specs for an object.
797

798
  At most one of new_specs or diff_specs can be specified.
799

800
  @type new_specs: dict
801
  @param new_specs: new complete specs, in the same format returned by
802
      L{ParseIPolicy}.
803
  @type diff_specs: dict
804
  @param diff_specs: partial specs, it can be an incomplete specifications, but
805
      if min/max specs are specified, their number must match the number of the
806
      existing specs
807
  @type get_policy_fn: function
808
  @param get_policy_fn: function that returns the current policy as in
809
      L{ParseIPolicy}
810
  @type build_cmd_fn: function
811
  @param build_cmd_fn: function that return the full command line from the
812
      options alone
813
  @type fail: bool
814
  @param fail: if the change is expected to fail
815
  @type old_values: tuple
816
  @param old_values: (old_policy, old_specs), as returned by
817
     L{ParseIPolicy}
818
  @return: same as L{ParseIPolicy}
819

820
  """
821
  assert get_policy_fn is not None
822
  assert build_cmd_fn is not None
823
  assert new_specs is None or diff_specs is None
824

    
825
  if old_values:
826
    (old_policy, old_specs) = old_values
827
  else:
828
    (old_policy, old_specs) = get_policy_fn()
829

    
830
  if diff_specs:
831
    new_specs = copy.deepcopy(old_specs)
832
    if constants.ISPECS_MINMAX in diff_specs:
833
      AssertEqual(len(new_specs[constants.ISPECS_MINMAX]),
834
                  len(diff_specs[constants.ISPECS_MINMAX]))
835
      for (new_minmax, diff_minmax) in zip(new_specs[constants.ISPECS_MINMAX],
836
                                           diff_specs[constants.ISPECS_MINMAX]):
837
        for (key, parvals) in diff_minmax.items():
838
          for (par, val) in parvals.items():
839
            new_minmax[key][par] = val
840
    for (par, val) in diff_specs.get(constants.ISPECS_STD, {}).items():
841
      new_specs[constants.ISPECS_STD][par] = val
842

    
843
  if new_specs:
844
    cmd = []
845
    if (diff_specs is None or constants.ISPECS_MINMAX in diff_specs):
846
      minmax_opt_items = []
847
      for minmax in new_specs[constants.ISPECS_MINMAX]:
848
        minmax_opts = []
849
        for key in ["min", "max"]:
850
          keyopt = _GetParameterOptions(minmax[key])
851
          minmax_opts.append("%s:%s" % (key, keyopt))
852
        minmax_opt_items.append("/".join(minmax_opts))
853
      cmd.extend([
854
        "--ipolicy-bounds-specs",
855
        "//".join(minmax_opt_items)
856
        ])
857
    if diff_specs is None:
858
      std_source = new_specs
859
    else:
860
      std_source = diff_specs
861
    std_opt = _GetParameterOptions(std_source.get("std", {}))
862
    if std_opt:
863
      cmd.extend(["--ipolicy-std-specs", std_opt])
864
    AssertCommand(build_cmd_fn(cmd), fail=fail)
865

    
866
    # Check the new state
867
    (eff_policy, eff_specs) = get_policy_fn()
868
    AssertEqual(eff_policy, old_policy)
869
    if fail:
870
      AssertEqual(eff_specs, old_specs)
871
    else:
872
      AssertEqual(eff_specs, new_specs)
873

    
874
  else:
875
    (eff_policy, eff_specs) = (old_policy, old_specs)
876

    
877
  return (eff_policy, eff_specs)
878

    
879

    
880
def ParseIPolicy(policy):
881
  """Parse and split instance an instance policy.
882

883
  @type policy: dict
884
  @param policy: policy, as returned by L{GetObjectInfo}
885
  @rtype: tuple
886
  @return: (policy, specs), where:
887
      - policy is a dictionary of the policy values, instance specs excluded
888
      - specs is a dictionary containing only the specs, using the internal
889
        format (see L{constants.IPOLICY_DEFAULTS} for an example)
890

891
  """
892
  ret_specs = {}
893
  ret_policy = {}
894
  for (key, val) in policy.items():
895
    if key == "bounds specs":
896
      ret_specs[constants.ISPECS_MINMAX] = []
897
      for minmax in val:
898
        ret_minmax = {}
899
        for key in minmax:
900
          keyparts = key.split("/", 1)
901
          assert len(keyparts) > 1
902
          ret_minmax[keyparts[0]] = minmax[key]
903
        ret_specs[constants.ISPECS_MINMAX].append(ret_minmax)
904
    elif key == constants.ISPECS_STD:
905
      ret_specs[key] = val
906
    else:
907
      ret_policy[key] = val
908
  return (ret_policy, ret_specs)