Statistics
| Branch: | Tag: | Revision:

root / qa / qa_utils.py @ 1a732a74

History | View | Annotate | Download (25 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=$(tempfile --mode %o --prefix gnt) && '
374
         '[[ -f "${tmp}" ]] && '
375
         'cat > "${tmp}" && '
376
         'echo "${tmp}"') % mode
377

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

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

    
389

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

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

396
  """
397
  if filename:
398
    tmp = "tmp=%s" % utils.ShellQuote(filename)
399
  else:
400
    tmp = "tmp=$(tempfile --mode %o --prefix gnt)" % mode
401
  cmd = ("%s && "
402
         "[[ -f \"${tmp}\" ]] && "
403
         "cat > \"${tmp}\" && "
404
         "echo \"${tmp}\"") % tmp
405

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

    
412
  # Return temporary filename
413
  return p.stdout.read().strip()
414

    
415

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

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

422
  """
423
  vpath = MakeNodePath(node, path)
424

    
425
  cmd = ("tmp=$(tempfile --prefix .gnt --directory=$(dirname %s)) && "
426
         "[[ -f \"$tmp\" ]] && "
427
         "cp %s $tmp && "
428
         "echo $tmp") % (utils.ShellQuote(vpath), utils.ShellQuote(vpath))
429

    
430
  # Return temporary filename
431
  result = GetCommandOutput(node, cmd).strip()
432

    
433
  print "Backup filename: %s" % result
434

    
435
  return result
436

    
437

    
438
def ResolveInstanceName(instance):
439
  """Gets the full name of an instance.
440

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

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

    
448

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

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

    
456

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

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

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

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

    
476
  return instances
477

    
478

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

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

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

    
491
  # Duplicate fields
492
  yield fields + fields
493

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

    
498

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

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

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

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

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

    
514

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

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

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

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

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

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

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

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

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

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

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

    
557

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

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

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

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

    
577

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

    
583

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

    
588

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

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

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

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

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

    
613

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

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

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

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

    
635

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

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

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

    
646
  master_node = qa_config.GetMasterNode()
647

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

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

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

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

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

    
674

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

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

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

    
691

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

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

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

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

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

    
712
      return result
713
    return wrapper
714
  return decorator
715

    
716

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

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

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

    
726

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

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

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

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

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

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

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

    
757
  return candidates
758

    
759

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

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

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

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

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

    
782

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

    
789

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

794
  At most one of new_specs or diff_specs can be specified.
795

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

816
  """
817
  assert get_policy_fn is not None
818
  assert build_cmd_fn is not None
819
  assert new_specs is None or diff_specs is None
820

    
821
  if old_values:
822
    (old_policy, old_specs) = old_values
823
  else:
824
    (old_policy, old_specs) = get_policy_fn()
825

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

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

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

    
870
  else:
871
    (eff_policy, eff_specs) = (old_policy, old_specs)
872

    
873
  return (eff_policy, eff_specs)
874

    
875

    
876
def ParseIPolicy(policy):
877
  """Parse and split instance an instance policy.
878

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

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