Statistics
| Branch: | Tag: | Revision:

root / qa / qa_utils.py @ bfbef99b

History | View | Annotate | Download (25.5 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 _AssertListNoUnknownValues(listcmd, fields):
518
  """Assert that the list command does not output unknown values.
519

520
  """
521
  master = qa_config.GetMasterNode()
522

    
523
  cmd = [listcmd, "list", "--output", ",".join(fields)]
524

    
525
  output = GetCommandOutput(master.primary,
526
                            utils.ShellQuoteArgs(cmd)).splitlines()
527
  for line in output:
528
    AssertNotIn('?', line)
529

    
530

    
531
def GenericQueryTest(cmd, fields, namefield="name", test_unknown=True,
532
                     test_values_unknown=True):
533
  """Runs a number of tests on query commands.
534

535
  @param cmd: Command name
536
  @param fields: List of field names
537

538
  """
539
  rnd = random.Random(hash(cmd))
540

    
541
  fields = list(fields)
542
  rnd.shuffle(fields)
543

    
544
  # Test a number of field combinations
545
  for testfields in _SelectQueryFields(rnd, fields):
546
    AssertRedirectedCommand([cmd, "list", "--output", ",".join(testfields)])
547

    
548
  if namefield is not None:
549
    namelist_fn = compat.partial(_List, cmd, [namefield])
550

    
551
    # When no names were requested, the list must be sorted
552
    names = namelist_fn(None)
553
    AssertEqual(names, utils.NiceSort(names))
554

    
555
    # When requesting specific names, the order must be kept
556
    revnames = list(reversed(names))
557
    AssertEqual(namelist_fn(revnames), revnames)
558

    
559
    randnames = list(names)
560
    rnd.shuffle(randnames)
561
    AssertEqual(namelist_fn(randnames), randnames)
562

    
563
  if test_unknown:
564
    # Listing unknown items must fail
565
    AssertCommand([cmd, "list", "this.name.certainly.does.not.exist"],
566
                  fail=True)
567

    
568
  if test_values_unknown:
569
    _AssertListNoUnknownValues(cmd, fields)
570

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

    
577

    
578
def GenericQueryFieldsTest(cmd, fields):
579
  master = qa_config.GetMasterNode()
580

    
581
  # Listing fields
582
  AssertRedirectedCommand([cmd, "list-fields"])
583
  AssertRedirectedCommand([cmd, "list-fields"] + fields)
584

    
585
  # Check listed fields (all, must be sorted)
586
  realcmd = [cmd, "list-fields", "--separator=|", "--no-headers"]
587
  output = GetCommandOutput(master.primary,
588
                            utils.ShellQuoteArgs(realcmd)).splitlines()
589
  AssertEqual([line.split("|", 1)[0] for line in output],
590
              utils.NiceSort(fields))
591

    
592
  # Check exit code for listing unknown field
593
  AssertEqual(AssertCommand([cmd, "list-fields", "field/does/not/exist"],
594
                            fail=True),
595
              constants.EXIT_UNKNOWN_FIELD)
596

    
597

    
598
def _FormatWithColor(text, seq):
599
  if not seq:
600
    return text
601
  return "%s%s%s" % (seq, text, _RESET_SEQ)
602

    
603

    
604
FormatWarning = lambda text: _FormatWithColor(text, _WARNING_SEQ)
605
FormatError = lambda text: _FormatWithColor(text, _ERROR_SEQ)
606
FormatInfo = lambda text: _FormatWithColor(text, _INFO_SEQ)
607

    
608

    
609
def AddToEtcHosts(hostnames):
610
  """Adds hostnames to /etc/hosts.
611

612
  @param hostnames: List of hostnames first used A records, all other CNAMEs
613

614
  """
615
  master = qa_config.GetMasterNode()
616
  tmp_hosts = UploadData(master.primary, "", mode=0644)
617

    
618
  data = []
619
  for localhost in ("::1", "127.0.0.1"):
620
    data.append("%s %s" % (localhost, " ".join(hostnames)))
621

    
622
  try:
623
    AssertCommand("{ cat %s && echo -e '%s'; } > %s && mv %s %s" %
624
                  (utils.ShellQuote(pathutils.ETC_HOSTS),
625
                   "\\n".join(data),
626
                   utils.ShellQuote(tmp_hosts),
627
                   utils.ShellQuote(tmp_hosts),
628
                   utils.ShellQuote(pathutils.ETC_HOSTS)))
629
  except Exception:
630
    AssertCommand(["rm", "-f", tmp_hosts])
631
    raise
632

    
633

    
634
def RemoveFromEtcHosts(hostnames):
635
  """Remove hostnames from /etc/hosts.
636

637
  @param hostnames: List of hostnames first used A records, all other CNAMEs
638

639
  """
640
  master = qa_config.GetMasterNode()
641
  tmp_hosts = UploadData(master.primary, "", mode=0644)
642
  quoted_tmp_hosts = utils.ShellQuote(tmp_hosts)
643

    
644
  sed_data = " ".join(hostnames)
645
  try:
646
    AssertCommand(("sed -e '/^\(::1\|127\.0\.0\.1\)\s\+%s/d' %s > %s"
647
                   " && mv %s %s") %
648
                   (sed_data, utils.ShellQuote(pathutils.ETC_HOSTS),
649
                    quoted_tmp_hosts, quoted_tmp_hosts,
650
                    utils.ShellQuote(pathutils.ETC_HOSTS)))
651
  except Exception:
652
    AssertCommand(["rm", "-f", tmp_hosts])
653
    raise
654

    
655

    
656
def RunInstanceCheck(instance, running):
657
  """Check if instance is running or not.
658

659
  """
660
  instance_name = _GetName(instance, operator.attrgetter("name"))
661

    
662
  script = qa_config.GetInstanceCheckScript()
663
  if not script:
664
    return
665

    
666
  master_node = qa_config.GetMasterNode()
667

    
668
  # Build command to connect to master node
669
  master_ssh = GetSSHCommand(master_node.primary, "--")
670

    
671
  if running:
672
    running_shellval = "1"
673
    running_text = ""
674
  else:
675
    running_shellval = ""
676
    running_text = "not "
677

    
678
  print FormatInfo("Checking if instance '%s' is %srunning" %
679
                   (instance_name, running_text))
680

    
681
  args = [script, instance_name]
682
  env = {
683
    "PATH": constants.HOOKS_PATH,
684
    "RUN_UUID": _RUN_UUID,
685
    "MASTER_SSH": utils.ShellQuoteArgs(master_ssh),
686
    "INSTANCE_NAME": instance_name,
687
    "INSTANCE_RUNNING": running_shellval,
688
    }
689

    
690
  result = os.spawnve(os.P_WAIT, script, args, env)
691
  if result != 0:
692
    raise qa_error.Error("Instance check failed with result %s" % result)
693

    
694

    
695
def _InstanceCheckInner(expected, instarg, args, result):
696
  """Helper function used by L{InstanceCheck}.
697

698
  """
699
  if instarg == FIRST_ARG:
700
    instance = args[0]
701
  elif instarg == RETURN_VALUE:
702
    instance = result
703
  else:
704
    raise Exception("Invalid value '%s' for instance argument" % instarg)
705

    
706
  if expected in (INST_DOWN, INST_UP):
707
    RunInstanceCheck(instance, (expected == INST_UP))
708
  elif expected is not None:
709
    raise Exception("Invalid value '%s'" % expected)
710

    
711

    
712
def InstanceCheck(before, after, instarg):
713
  """Decorator to check instance status before and after test.
714

715
  @param before: L{INST_DOWN} if instance must be stopped before test,
716
    L{INST_UP} if instance must be running before test, L{None} to not check.
717
  @param after: L{INST_DOWN} if instance must be stopped after test,
718
    L{INST_UP} if instance must be running after test, L{None} to not check.
719
  @param instarg: L{FIRST_ARG} to use first argument to test as instance (a
720
    dictionary), L{RETURN_VALUE} to use return value (disallows pre-checks)
721

722
  """
723
  def decorator(fn):
724
    @functools.wraps(fn)
725
    def wrapper(*args, **kwargs):
726
      _InstanceCheckInner(before, instarg, args, NotImplemented)
727

    
728
      result = fn(*args, **kwargs)
729

    
730
      _InstanceCheckInner(after, instarg, args, result)
731

    
732
      return result
733
    return wrapper
734
  return decorator
735

    
736

    
737
def GetNonexistentGroups(count):
738
  """Gets group names which shouldn't exist on the cluster.
739

740
  @param count: Number of groups to get
741
  @rtype: integer
742

743
  """
744
  return GetNonexistentEntityNames(count, "groups", "group")
745

    
746

    
747
def GetNonexistentEntityNames(count, name_config, name_prefix):
748
  """Gets entity names which shouldn't exist on the cluster.
749

750
  The actualy names can refer to arbitrary entities (for example
751
  groups, networks).
752

753
  @param count: Number of names to get
754
  @rtype: integer
755
  @param name_config: name of the leaf in the config containing
756
    this entity's configuration, including a 'inexistent-'
757
    element
758
  @rtype: string
759
  @param name_prefix: prefix of the entity's names, used to compose
760
    the default values; for example for groups, the prefix is
761
    'group' and the generated names are then group1, group2, ...
762
  @rtype: string
763

764
  """
765
  entities = qa_config.get(name_config, {})
766

    
767
  default = [name_prefix + str(i) for i in range(count)]
768
  assert count <= len(default)
769

    
770
  name_config_inexistent = "inexistent-" + name_config
771
  candidates = entities.get(name_config_inexistent, default)[:count]
772

    
773
  if len(candidates) < count:
774
    raise Exception("At least %s non-existent %s are needed" %
775
                    (count, name_config))
776

    
777
  return candidates
778

    
779

    
780
def MakeNodePath(node, path):
781
  """Builds an absolute path for a virtual node.
782

783
  @type node: string or L{qa_config._QaNode}
784
  @param node: Node
785
  @type path: string
786
  @param path: Path without node-specific prefix
787

788
  """
789
  (_, basedir) = qa_config.GetVclusterSettings()
790

    
791
  if isinstance(node, basestring):
792
    name = node
793
  else:
794
    name = node.primary
795

    
796
  if basedir:
797
    assert path.startswith("/")
798
    return "%s%s" % (vcluster.MakeNodeRoot(basedir, name), path)
799
  else:
800
    return path
801

    
802

    
803
def _GetParameterOptions(specs):
804
  """Helper to build policy options."""
805
  values = ["%s=%s" % (par, val)
806
            for (par, val) in specs.items()]
807
  return ",".join(values)
808

    
809

    
810
def TestSetISpecs(new_specs=None, diff_specs=None, get_policy_fn=None,
811
                  build_cmd_fn=None, fail=False, old_values=None):
812
  """Change instance specs for an object.
813

814
  At most one of new_specs or diff_specs can be specified.
815

816
  @type new_specs: dict
817
  @param new_specs: new complete specs, in the same format returned by
818
      L{ParseIPolicy}.
819
  @type diff_specs: dict
820
  @param diff_specs: partial specs, it can be an incomplete specifications, but
821
      if min/max specs are specified, their number must match the number of the
822
      existing specs
823
  @type get_policy_fn: function
824
  @param get_policy_fn: function that returns the current policy as in
825
      L{ParseIPolicy}
826
  @type build_cmd_fn: function
827
  @param build_cmd_fn: function that return the full command line from the
828
      options alone
829
  @type fail: bool
830
  @param fail: if the change is expected to fail
831
  @type old_values: tuple
832
  @param old_values: (old_policy, old_specs), as returned by
833
     L{ParseIPolicy}
834
  @return: same as L{ParseIPolicy}
835

836
  """
837
  assert get_policy_fn is not None
838
  assert build_cmd_fn is not None
839
  assert new_specs is None or diff_specs is None
840

    
841
  if old_values:
842
    (old_policy, old_specs) = old_values
843
  else:
844
    (old_policy, old_specs) = get_policy_fn()
845

    
846
  if diff_specs:
847
    new_specs = copy.deepcopy(old_specs)
848
    if constants.ISPECS_MINMAX in diff_specs:
849
      AssertEqual(len(new_specs[constants.ISPECS_MINMAX]),
850
                  len(diff_specs[constants.ISPECS_MINMAX]))
851
      for (new_minmax, diff_minmax) in zip(new_specs[constants.ISPECS_MINMAX],
852
                                           diff_specs[constants.ISPECS_MINMAX]):
853
        for (key, parvals) in diff_minmax.items():
854
          for (par, val) in parvals.items():
855
            new_minmax[key][par] = val
856
    for (par, val) in diff_specs.get(constants.ISPECS_STD, {}).items():
857
      new_specs[constants.ISPECS_STD][par] = val
858

    
859
  if new_specs:
860
    cmd = []
861
    if (diff_specs is None or constants.ISPECS_MINMAX in diff_specs):
862
      minmax_opt_items = []
863
      for minmax in new_specs[constants.ISPECS_MINMAX]:
864
        minmax_opts = []
865
        for key in ["min", "max"]:
866
          keyopt = _GetParameterOptions(minmax[key])
867
          minmax_opts.append("%s:%s" % (key, keyopt))
868
        minmax_opt_items.append("/".join(minmax_opts))
869
      cmd.extend([
870
        "--ipolicy-bounds-specs",
871
        "//".join(minmax_opt_items)
872
        ])
873
    if diff_specs is None:
874
      std_source = new_specs
875
    else:
876
      std_source = diff_specs
877
    std_opt = _GetParameterOptions(std_source.get("std", {}))
878
    if std_opt:
879
      cmd.extend(["--ipolicy-std-specs", std_opt])
880
    AssertCommand(build_cmd_fn(cmd), fail=fail)
881

    
882
    # Check the new state
883
    (eff_policy, eff_specs) = get_policy_fn()
884
    AssertEqual(eff_policy, old_policy)
885
    if fail:
886
      AssertEqual(eff_specs, old_specs)
887
    else:
888
      AssertEqual(eff_specs, new_specs)
889

    
890
  else:
891
    (eff_policy, eff_specs) = (old_policy, old_specs)
892

    
893
  return (eff_policy, eff_specs)
894

    
895

    
896
def ParseIPolicy(policy):
897
  """Parse and split instance an instance policy.
898

899
  @type policy: dict
900
  @param policy: policy, as returned by L{GetObjectInfo}
901
  @rtype: tuple
902
  @return: (policy, specs), where:
903
      - policy is a dictionary of the policy values, instance specs excluded
904
      - specs is a dictionary containing only the specs, using the internal
905
        format (see L{constants.IPOLICY_DEFAULTS} for an example)
906

907
  """
908
  ret_specs = {}
909
  ret_policy = {}
910
  for (key, val) in policy.items():
911
    if key == "bounds specs":
912
      ret_specs[constants.ISPECS_MINMAX] = []
913
      for minmax in val:
914
        ret_minmax = {}
915
        for key in minmax:
916
          keyparts = key.split("/", 1)
917
          assert len(keyparts) > 1
918
          ret_minmax[keyparts[0]] = minmax[key]
919
        ret_specs[constants.ISPECS_MINMAX].append(ret_minmax)
920
    elif key == constants.ISPECS_STD:
921
      ret_specs[key] = val
922
    else:
923
      ret_policy[key] = val
924
  return (ret_policy, ret_specs)