Statistics
| Branch: | Tag: | Revision:

root / qa / qa_utils.py @ b780c231

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 qa_config
49
import qa_error
50

    
51

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

    
57
_MULTIPLEXERS = {}
58

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

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

    
65

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

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

    
72

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

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

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

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

    
91
  curses.setupterm()
92

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

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

    
100

    
101
_SetupColours()
102

    
103

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

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

    
111

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

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

    
119

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

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

    
127

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

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

    
135

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

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

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

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

    
151
  return result
152

    
153

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

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

    
165

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

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

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

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

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

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

    
195
  return rcode
196

    
197

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

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

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

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

    
218

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

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

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

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

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

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

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

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

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

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

    
276
  return args
277

    
278

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

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

    
291

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

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

    
299

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

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

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

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

    
317

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

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

    
327

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

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

    
347

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

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

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

    
362

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

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

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

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

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

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

    
390

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

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

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

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

    
414
  # Return temporary filename
415
  return p.stdout.read().strip()
416

    
417

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

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

424
  """
425
  vpath = MakeNodePath(node, path)
426

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

    
432
  # Return temporary filename
433
  result = GetCommandOutput(node, cmd).strip()
434

    
435
  print "Backup filename: %s" % result
436

    
437
  return result
438

    
439

    
440
def ResolveInstanceName(instance):
441
  """Gets the full name of an instance.
442

443
  @type instance: string
444
  @param instance: Instance name
445

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

    
450

    
451
def ResolveNodeName(node):
452
  """Gets the full name of a node.
453

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

    
458

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

462
  """
463
  master = qa_config.GetMasterNode()
464
  node_name = ResolveNodeName(node)
465

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

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

    
478
  return instances
479

    
480

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

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

    
489
  # Check all fields
490
  yield fields
491
  yield sorted(fields)
492

    
493
  # Duplicate fields
494
  yield fields + fields
495

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

    
500

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

504
  """
505
  master = qa_config.GetMasterNode()
506

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

    
510
  if names:
511
    cmd.extend(names)
512

    
513
  return GetCommandOutput(master.primary,
514
                          utils.ShellQuoteArgs(cmd)).splitlines()
515

    
516

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

520
  @param cmd: Command name
521
  @param fields: List of field names
522

523
  """
524
  rnd = random.Random(hash(cmd))
525

    
526
  fields = list(fields)
527
  rnd.shuffle(fields)
528

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

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

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

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

    
544
    randnames = list(names)
545
    rnd.shuffle(randnames)
546
    AssertEqual(namelist_fn(randnames), randnames)
547

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

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

    
559

    
560
def GenericQueryFieldsTest(cmd, fields):
561
  master = qa_config.GetMasterNode()
562

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

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

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

    
579

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

    
585

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

    
590

    
591
def AddToEtcHosts(hostnames):
592
  """Adds hostnames to /etc/hosts.
593

594
  @param hostnames: List of hostnames first used A records, all other CNAMEs
595

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

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

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

    
615

    
616
def RemoveFromEtcHosts(hostnames):
617
  """Remove hostnames from /etc/hosts.
618

619
  @param hostnames: List of hostnames first used A records, all other CNAMEs
620

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

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

    
637

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

641
  """
642
  instance_name = _GetName(instance, operator.attrgetter("name"))
643

    
644
  script = qa_config.GetInstanceCheckScript()
645
  if not script:
646
    return
647

    
648
  master_node = qa_config.GetMasterNode()
649

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

    
653
  if running:
654
    running_shellval = "1"
655
    running_text = ""
656
  else:
657
    running_shellval = ""
658
    running_text = "not "
659

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

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

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

    
676

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

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

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

    
693

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

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

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

    
710
      result = fn(*args, **kwargs)
711

    
712
      _InstanceCheckInner(after, instarg, args, result)
713

    
714
      return result
715
    return wrapper
716
  return decorator
717

    
718

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

722
  @param count: Number of groups to get
723
  @rtype: integer
724

725
  """
726
  return GetNonexistentEntityNames(count, "groups", "group")
727

    
728

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

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

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

746
  """
747
  entities = qa_config.get(name_config, {})
748

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

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

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

    
759
  return candidates
760

    
761

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

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

770
  """
771
  (_, basedir) = qa_config.GetVclusterSettings()
772

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

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

    
784

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

    
791

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

796
  At most one of new_specs or diff_specs can be specified.
797

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

818
  """
819
  assert get_policy_fn is not None
820
  assert build_cmd_fn is not None
821
  assert new_specs is None or diff_specs is None
822

    
823
  if old_values:
824
    (old_policy, old_specs) = old_values
825
  else:
826
    (old_policy, old_specs) = get_policy_fn()
827

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

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

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

    
872
  else:
873
    (eff_policy, eff_specs) = (old_policy, old_specs)
874

    
875
  return (eff_policy, eff_specs)
876

    
877

    
878
def ParseIPolicy(policy):
879
  """Parse and split instance an instance policy.
880

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

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