Statistics
| Branch: | Tag: | Revision:

root / qa / qa_utils.py @ 67bd83ae

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 socket
32
import subprocess
33
import sys
34
import tempfile
35
import yaml
36

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

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

    
49
import colors
50
import qa_config
51
import qa_error
52

    
53

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

    
59
_MULTIPLEXERS = {}
60

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

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

    
67

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

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

    
74

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

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

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

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

    
93
  try:
94
    curses.setupterm()
95
  except curses.error:
96
    # Probably a non-standard terminal, don't use colours then
97
    return
98

    
99
  _RESET_SEQ = curses.tigetstr("op")
100

    
101
  setaf = curses.tigetstr("setaf")
102
  _INFO_SEQ = curses.tparm(setaf, curses.COLOR_GREEN)
103
  _WARNING_SEQ = curses.tparm(setaf, curses.COLOR_YELLOW)
104
  _ERROR_SEQ = curses.tparm(setaf, curses.COLOR_RED)
105

    
106

    
107
_SetupColours()
108

    
109

    
110
def AssertIn(item, sequence):
111
  """Raises an error when item is not in sequence.
112

113
  """
114
  if item not in sequence:
115
    raise qa_error.Error("%r not in %r" % (item, sequence))
116

    
117

    
118
def AssertNotIn(item, sequence):
119
  """Raises an error when item is in sequence.
120

121
  """
122
  if item in sequence:
123
    raise qa_error.Error("%r in %r" % (item, sequence))
124

    
125

    
126
def AssertEqual(first, second, msg=""):
127
  """Raises an error when values aren't equal.
128

129
  """
130
  if not first == second:
131
    if msg:
132
      raise qa_error.Error("%s: %r == %r" % (msg, first, second))
133
    else:
134
      raise qa_error.Error("%r == %r" % (first, second))
135

    
136

    
137
def AssertMatch(string, pattern):
138
  """Raises an error when string doesn't match regexp pattern.
139

140
  """
141
  if not re.match(pattern, string):
142
    raise qa_error.Error("%r doesn't match /%r/" % (string, pattern))
143

    
144

    
145
def _GetName(entity, fn):
146
  """Tries to get name of an entity.
147

148
  @type entity: string or dict
149
  @param fn: Function retrieving name from entity
150

151
  """
152
  if isinstance(entity, basestring):
153
    result = entity
154
  else:
155
    result = fn(entity)
156

    
157
  if not ht.TNonEmptyString(result):
158
    raise Exception("Invalid name '%s'" % result)
159

    
160
  return result
161

    
162

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

166
  """
167
  if fail and rcode == 0:
168
    raise qa_error.Error("Command '%s' on node %s was expected to fail but"
169
                         " didn't" % (cmdstr, nodename))
170
  elif not fail and rcode != 0:
171
    raise qa_error.Error("Command '%s' on node %s failed, exit code %s" %
172
                         (cmdstr, nodename, rcode))
173

    
174

    
175
def AssertCommand(cmd, fail=False, node=None, log_cmd=True):
176
  """Checks that a remote command succeeds.
177

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

190
  """
191
  if node is None:
192
    node = qa_config.GetMasterNode()
193

    
194
  nodename = _GetName(node, operator.attrgetter("primary"))
195

    
196
  if isinstance(cmd, basestring):
197
    cmdstr = cmd
198
  else:
199
    cmdstr = utils.ShellQuoteArgs(cmd)
200

    
201
  rcode = StartSSH(nodename, cmdstr, log_cmd=log_cmd).wait()
202
  _AssertRetCode(rcode, fail, cmdstr, nodename)
203

    
204
  return rcode
205

    
206

    
207
def AssertRedirectedCommand(cmd, fail=False, node=None, log_cmd=True):
208
  """Executes a command with redirected output.
209

210
  The log will go to the qa-output log file in the ganeti log
211
  directory on the node where the command is executed. The fail and
212
  node parameters are passed unchanged to AssertCommand.
213

214
  @param cmd: the command to be executed, as a list; a string is not
215
      supported
216

217
  """
218
  if not isinstance(cmd, list):
219
    raise qa_error.Error("Non-list passed to AssertRedirectedCommand")
220
  ofile = utils.ShellQuote(_QA_OUTPUT)
221
  cmdstr = utils.ShellQuoteArgs(cmd)
222
  AssertCommand("echo ---- $(date) %s ---- >> %s" % (cmdstr, ofile),
223
                fail=False, node=node, log_cmd=False)
224
  return AssertCommand(cmdstr + " >> %s" % ofile,
225
                       fail=fail, node=node, log_cmd=log_cmd)
226

    
227

    
228
def GetSSHCommand(node, cmd, strict=True, opts=None, tty=None):
229
  """Builds SSH command to be executed.
230

231
  @type node: string
232
  @param node: node the command should run on
233
  @type cmd: string
234
  @param cmd: command to be executed in the node; if None or empty
235
      string, no command will be executed
236
  @type strict: boolean
237
  @param strict: whether to enable strict host key checking
238
  @type opts: list
239
  @param opts: list of additional options
240
  @type tty: boolean or None
241
  @param tty: if we should use tty; if None, will be auto-detected
242

243
  """
244
  args = ["ssh", "-oEscapeChar=none", "-oBatchMode=yes", "-lroot"]
245

    
246
  if tty is None:
247
    tty = sys.stdout.isatty()
248

    
249
  if tty:
250
    args.append("-t")
251

    
252
  if strict:
253
    tmp = "yes"
254
  else:
255
    tmp = "no"
256
  args.append("-oStrictHostKeyChecking=%s" % tmp)
257
  args.append("-oClearAllForwardings=yes")
258
  args.append("-oForwardAgent=yes")
259
  if opts:
260
    args.extend(opts)
261
  if node in _MULTIPLEXERS:
262
    spath = _MULTIPLEXERS[node][0]
263
    args.append("-oControlPath=%s" % spath)
264
    args.append("-oControlMaster=no")
265

    
266
  (vcluster_master, vcluster_basedir) = \
267
    qa_config.GetVclusterSettings()
268

    
269
  if vcluster_master:
270
    args.append(vcluster_master)
271
    args.append("%s/%s/cmd" % (vcluster_basedir, node))
272

    
273
    if cmd:
274
      # For virtual clusters the whole command must be wrapped using the "cmd"
275
      # script, as that script sets a number of environment variables. If the
276
      # command contains shell meta characters the whole command needs to be
277
      # quoted.
278
      args.append(utils.ShellQuote(cmd))
279
  else:
280
    args.append(node)
281

    
282
    if cmd:
283
      args.append(cmd)
284

    
285
  return args
286

    
287

    
288
def StartLocalCommand(cmd, _nolog_opts=False, log_cmd=True, **kwargs):
289
  """Starts a local command.
290

291
  """
292
  if log_cmd:
293
    if _nolog_opts:
294
      pcmd = [i for i in cmd if not i.startswith("-")]
295
    else:
296
      pcmd = cmd
297
    print "%s %s" % (colors.colorize("Command:", colors.CYAN),
298
                     utils.ShellQuoteArgs(pcmd))
299
  return subprocess.Popen(cmd, shell=False, **kwargs)
300

    
301

    
302
def StartSSH(node, cmd, strict=True, log_cmd=True):
303
  """Starts SSH.
304

305
  """
306
  return StartLocalCommand(GetSSHCommand(node, cmd, strict=strict),
307
                           _nolog_opts=True, log_cmd=log_cmd)
308

    
309

    
310
def StartMultiplexer(node):
311
  """Starts a multiplexer command.
312

313
  @param node: the node for which to open the multiplexer
314

315
  """
316
  if node in _MULTIPLEXERS:
317
    return
318

    
319
  # Note: yes, we only need mktemp, since we'll remove the file anyway
320
  sname = tempfile.mktemp(prefix="ganeti-qa-multiplexer.")
321
  utils.RemoveFile(sname)
322
  opts = ["-N", "-oControlPath=%s" % sname, "-oControlMaster=yes"]
323
  print "Created socket at %s" % sname
324
  child = StartLocalCommand(GetSSHCommand(node, None, opts=opts))
325
  _MULTIPLEXERS[node] = (sname, child)
326

    
327

    
328
def CloseMultiplexers():
329
  """Closes all current multiplexers and cleans up.
330

331
  """
332
  for node in _MULTIPLEXERS.keys():
333
    (sname, child) = _MULTIPLEXERS.pop(node)
334
    utils.KillProcess(child.pid, timeout=10, waitpid=True)
335
    utils.RemoveFile(sname)
336

    
337

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

341
  @type node: string
342
  @param node: node the command should run on
343
  @type cmd: string
344
  @param cmd: command to be executed in the node (cannot be empty or None)
345
  @type tty: bool or None
346
  @param tty: if we should use tty; if None, it will be auto-detected
347
  @type fail: bool
348
  @param fail: whether the command is expected to fail
349
  """
350
  assert cmd
351
  p = StartLocalCommand(GetSSHCommand(node, cmd, tty=tty),
352
                        stdout=subprocess.PIPE)
353
  rcode = p.wait()
354
  _AssertRetCode(rcode, fail, cmd, node)
355
  return p.stdout.read()
356

    
357

    
358
def GetObjectInfo(infocmd):
359
  """Get and parse information about a Ganeti object.
360

361
  @type infocmd: list of strings
362
  @param infocmd: command to be executed, e.g. ["gnt-cluster", "info"]
363
  @return: the information parsed, appropriately stored in dictionaries,
364
      lists...
365

366
  """
367
  master = qa_config.GetMasterNode()
368
  cmdline = utils.ShellQuoteArgs(infocmd)
369
  info_out = GetCommandOutput(master.primary, cmdline)
370
  return yaml.load(info_out)
371

    
372

    
373
def UploadFile(node, src):
374
  """Uploads a file to a node and returns the filename.
375

376
  Caller needs to remove the returned file on the node when it's not needed
377
  anymore.
378

379
  """
380
  # Make sure nobody else has access to it while preserving local permissions
381
  mode = os.stat(src).st_mode & 0700
382

    
383
  cmd = ('tmp=$(mktemp --tmpdir gnt.XXXXXX) && '
384
         'chmod %o "${tmp}" && '
385
         '[[ -f "${tmp}" ]] && '
386
         'cat > "${tmp}" && '
387
         'echo "${tmp}"') % mode
388

    
389
  f = open(src, "r")
390
  try:
391
    p = subprocess.Popen(GetSSHCommand(node, cmd), shell=False, stdin=f,
392
                         stdout=subprocess.PIPE)
393
    AssertEqual(p.wait(), 0)
394

    
395
    # Return temporary filename
396
    return p.stdout.read().strip()
397
  finally:
398
    f.close()
399

    
400

    
401
def UploadData(node, data, mode=0600, filename=None):
402
  """Uploads data to a node and returns the filename.
403

404
  Caller needs to remove the returned file on the node when it's not needed
405
  anymore.
406

407
  """
408
  if filename:
409
    tmp = "tmp=%s" % utils.ShellQuote(filename)
410
  else:
411
    tmp = ('tmp=$(mktemp --tmpdir gnt.XXXXXX) && '
412
           'chmod %o "${tmp}"') % mode
413
  cmd = ("%s && "
414
         "[[ -f \"${tmp}\" ]] && "
415
         "cat > \"${tmp}\" && "
416
         "echo \"${tmp}\"") % tmp
417

    
418
  p = subprocess.Popen(GetSSHCommand(node, cmd), shell=False,
419
                       stdin=subprocess.PIPE, stdout=subprocess.PIPE)
420
  p.stdin.write(data)
421
  p.stdin.close()
422
  AssertEqual(p.wait(), 0)
423

    
424
  # Return temporary filename
425
  return p.stdout.read().strip()
426

    
427

    
428
def BackupFile(node, path):
429
  """Creates a backup of a file on the node and returns the filename.
430

431
  Caller needs to remove the returned file on the node when it's not needed
432
  anymore.
433

434
  """
435
  vpath = MakeNodePath(node, path)
436

    
437
  cmd = ("tmp=$(mktemp .gnt.XXXXXX --tmpdir=$(dirname %s)) && "
438
         "[[ -f \"$tmp\" ]] && "
439
         "cp %s $tmp && "
440
         "echo $tmp") % (utils.ShellQuote(vpath), utils.ShellQuote(vpath))
441

    
442
  # Return temporary filename
443
  result = GetCommandOutput(node, cmd).strip()
444

    
445
  print "Backup filename: %s" % result
446

    
447
  return result
448

    
449

    
450
def ResolveInstanceName(instance):
451
  """Gets the full name of an instance.
452

453
  @type instance: string
454
  @param instance: Instance name
455

456
  """
457
  info = GetObjectInfo(["gnt-instance", "info", instance])
458
  return info[0]["Instance name"]
459

    
460

    
461
def ResolveNodeName(node):
462
  """Gets the full name of a node.
463

464
  """
465
  info = GetObjectInfo(["gnt-node", "info", node.primary])
466
  return info[0]["Node name"]
467

    
468

    
469
def GetNodeInstances(node, secondaries=False):
470
  """Gets a list of instances on a node.
471

472
  """
473
  master = qa_config.GetMasterNode()
474
  node_name = ResolveNodeName(node)
475

    
476
  # Get list of all instances
477
  cmd = ["gnt-instance", "list", "--separator=:", "--no-headers",
478
         "--output=name,pnode,snodes"]
479
  output = GetCommandOutput(master.primary, utils.ShellQuoteArgs(cmd))
480

    
481
  instances = []
482
  for line in output.splitlines():
483
    (name, pnode, snodes) = line.split(":", 2)
484
    if ((not secondaries and pnode == node_name) or
485
        (secondaries and node_name in snodes.split(","))):
486
      instances.append(name)
487

    
488
  return instances
489

    
490

    
491
def _SelectQueryFields(rnd, fields):
492
  """Generates a list of fields for query tests.
493

494
  """
495
  # Create copy for shuffling
496
  fields = list(fields)
497
  rnd.shuffle(fields)
498

    
499
  # Check all fields
500
  yield fields
501
  yield sorted(fields)
502

    
503
  # Duplicate fields
504
  yield fields + fields
505

    
506
  # Check small groups of fields
507
  while fields:
508
    yield [fields.pop() for _ in range(rnd.randint(2, 10)) if fields]
509

    
510

    
511
def _List(listcmd, fields, names):
512
  """Runs a list command.
513

514
  """
515
  master = qa_config.GetMasterNode()
516

    
517
  cmd = [listcmd, "list", "--separator=|", "--no-headers",
518
         "--output", ",".join(fields)]
519

    
520
  if names:
521
    cmd.extend(names)
522

    
523
  return GetCommandOutput(master.primary,
524
                          utils.ShellQuoteArgs(cmd)).splitlines()
525

    
526

    
527
def GenericQueryTest(cmd, fields, namefield="name", test_unknown=True):
528
  """Runs a number of tests on query commands.
529

530
  @param cmd: Command name
531
  @param fields: List of field names
532

533
  """
534
  rnd = random.Random(hash(cmd))
535

    
536
  fields = list(fields)
537
  rnd.shuffle(fields)
538

    
539
  # Test a number of field combinations
540
  for testfields in _SelectQueryFields(rnd, fields):
541
    AssertRedirectedCommand([cmd, "list", "--output", ",".join(testfields)])
542

    
543
  if namefield is not None:
544
    namelist_fn = compat.partial(_List, cmd, [namefield])
545

    
546
    # When no names were requested, the list must be sorted
547
    names = namelist_fn(None)
548
    AssertEqual(names, utils.NiceSort(names))
549

    
550
    # When requesting specific names, the order must be kept
551
    revnames = list(reversed(names))
552
    AssertEqual(namelist_fn(revnames), revnames)
553

    
554
    randnames = list(names)
555
    rnd.shuffle(randnames)
556
    AssertEqual(namelist_fn(randnames), randnames)
557

    
558
  if test_unknown:
559
    # Listing unknown items must fail
560
    AssertCommand([cmd, "list", "this.name.certainly.does.not.exist"],
561
                  fail=True)
562

    
563
  # Check exit code for listing unknown field
564
  AssertEqual(AssertRedirectedCommand([cmd, "list",
565
                                       "--output=field/does/not/exist"],
566
                                      fail=True),
567
              constants.EXIT_UNKNOWN_FIELD)
568

    
569

    
570
def GenericQueryFieldsTest(cmd, fields):
571
  master = qa_config.GetMasterNode()
572

    
573
  # Listing fields
574
  AssertRedirectedCommand([cmd, "list-fields"])
575
  AssertRedirectedCommand([cmd, "list-fields"] + fields)
576

    
577
  # Check listed fields (all, must be sorted)
578
  realcmd = [cmd, "list-fields", "--separator=|", "--no-headers"]
579
  output = GetCommandOutput(master.primary,
580
                            utils.ShellQuoteArgs(realcmd)).splitlines()
581
  AssertEqual([line.split("|", 1)[0] for line in output],
582
              utils.NiceSort(fields))
583

    
584
  # Check exit code for listing unknown field
585
  AssertEqual(AssertCommand([cmd, "list-fields", "field/does/not/exist"],
586
                            fail=True),
587
              constants.EXIT_UNKNOWN_FIELD)
588

    
589

    
590
def _FormatWithColor(text, seq):
591
  if not seq:
592
    return text
593
  return "%s%s%s" % (seq, text, _RESET_SEQ)
594

    
595

    
596
FormatWarning = lambda text: _FormatWithColor(text, _WARNING_SEQ)
597
FormatError = lambda text: _FormatWithColor(text, _ERROR_SEQ)
598
FormatInfo = lambda text: _FormatWithColor(text, _INFO_SEQ)
599

    
600

    
601
def AddToEtcHosts(hostnames):
602
  """Adds hostnames to /etc/hosts.
603

604
  @param hostnames: List of hostnames first used A records, all other CNAMEs
605

606
  """
607
  master = qa_config.GetMasterNode()
608
  tmp_hosts = UploadData(master.primary, "", mode=0644)
609

    
610
  data = []
611
  for localhost in ("::1", "127.0.0.1"):
612
    data.append("%s %s" % (localhost, " ".join(hostnames)))
613

    
614
  try:
615
    AssertCommand("{ cat %s && echo -e '%s'; } > %s && mv %s %s" %
616
                  (utils.ShellQuote(pathutils.ETC_HOSTS),
617
                   "\\n".join(data),
618
                   utils.ShellQuote(tmp_hosts),
619
                   utils.ShellQuote(tmp_hosts),
620
                   utils.ShellQuote(pathutils.ETC_HOSTS)))
621
  except Exception:
622
    AssertCommand(["rm", "-f", tmp_hosts])
623
    raise
624

    
625

    
626
def RemoveFromEtcHosts(hostnames):
627
  """Remove hostnames from /etc/hosts.
628

629
  @param hostnames: List of hostnames first used A records, all other CNAMEs
630

631
  """
632
  master = qa_config.GetMasterNode()
633
  tmp_hosts = UploadData(master.primary, "", mode=0644)
634
  quoted_tmp_hosts = utils.ShellQuote(tmp_hosts)
635

    
636
  sed_data = " ".join(hostnames)
637
  try:
638
    AssertCommand((r"sed -e '/^\(::1\|127\.0\.0\.1\)\s\+%s/d' %s > %s"
639
                   r" && mv %s %s") %
640
                   (sed_data, utils.ShellQuote(pathutils.ETC_HOSTS),
641
                    quoted_tmp_hosts, quoted_tmp_hosts,
642
                    utils.ShellQuote(pathutils.ETC_HOSTS)))
643
  except Exception:
644
    AssertCommand(["rm", "-f", tmp_hosts])
645
    raise
646

    
647

    
648
def RunInstanceCheck(instance, running):
649
  """Check if instance is running or not.
650

651
  """
652
  instance_name = _GetName(instance, operator.attrgetter("name"))
653

    
654
  script = qa_config.GetInstanceCheckScript()
655
  if not script:
656
    return
657

    
658
  master_node = qa_config.GetMasterNode()
659

    
660
  # Build command to connect to master node
661
  master_ssh = GetSSHCommand(master_node.primary, "--")
662

    
663
  if running:
664
    running_shellval = "1"
665
    running_text = ""
666
  else:
667
    running_shellval = ""
668
    running_text = "not "
669

    
670
  print FormatInfo("Checking if instance '%s' is %srunning" %
671
                   (instance_name, running_text))
672

    
673
  args = [script, instance_name]
674
  env = {
675
    "PATH": constants.HOOKS_PATH,
676
    "RUN_UUID": _RUN_UUID,
677
    "MASTER_SSH": utils.ShellQuoteArgs(master_ssh),
678
    "INSTANCE_NAME": instance_name,
679
    "INSTANCE_RUNNING": running_shellval,
680
    }
681

    
682
  result = os.spawnve(os.P_WAIT, script, args, env)
683
  if result != 0:
684
    raise qa_error.Error("Instance check failed with result %s" % result)
685

    
686

    
687
def _InstanceCheckInner(expected, instarg, args, result):
688
  """Helper function used by L{InstanceCheck}.
689

690
  """
691
  if instarg == FIRST_ARG:
692
    instance = args[0]
693
  elif instarg == RETURN_VALUE:
694
    instance = result
695
  else:
696
    raise Exception("Invalid value '%s' for instance argument" % instarg)
697

    
698
  if expected in (INST_DOWN, INST_UP):
699
    RunInstanceCheck(instance, (expected == INST_UP))
700
  elif expected is not None:
701
    raise Exception("Invalid value '%s'" % expected)
702

    
703

    
704
def InstanceCheck(before, after, instarg):
705
  """Decorator to check instance status before and after test.
706

707
  @param before: L{INST_DOWN} if instance must be stopped before test,
708
    L{INST_UP} if instance must be running before test, L{None} to not check.
709
  @param after: L{INST_DOWN} if instance must be stopped after test,
710
    L{INST_UP} if instance must be running after test, L{None} to not check.
711
  @param instarg: L{FIRST_ARG} to use first argument to test as instance (a
712
    dictionary), L{RETURN_VALUE} to use return value (disallows pre-checks)
713

714
  """
715
  def decorator(fn):
716
    @functools.wraps(fn)
717
    def wrapper(*args, **kwargs):
718
      _InstanceCheckInner(before, instarg, args, NotImplemented)
719

    
720
      result = fn(*args, **kwargs)
721

    
722
      _InstanceCheckInner(after, instarg, args, result)
723

    
724
      return result
725
    return wrapper
726
  return decorator
727

    
728

    
729
def GetNonexistentGroups(count):
730
  """Gets group names which shouldn't exist on the cluster.
731

732
  @param count: Number of groups to get
733
  @rtype: integer
734

735
  """
736
  return GetNonexistentEntityNames(count, "groups", "group")
737

    
738

    
739
def GetNonexistentEntityNames(count, name_config, name_prefix):
740
  """Gets entity names which shouldn't exist on the cluster.
741

742
  The actualy names can refer to arbitrary entities (for example
743
  groups, networks).
744

745
  @param count: Number of names to get
746
  @rtype: integer
747
  @param name_config: name of the leaf in the config containing
748
    this entity's configuration, including a 'inexistent-'
749
    element
750
  @rtype: string
751
  @param name_prefix: prefix of the entity's names, used to compose
752
    the default values; for example for groups, the prefix is
753
    'group' and the generated names are then group1, group2, ...
754
  @rtype: string
755

756
  """
757
  entities = qa_config.get(name_config, {})
758

    
759
  default = [name_prefix + str(i) for i in range(count)]
760
  assert count <= len(default)
761

    
762
  name_config_inexistent = "inexistent-" + name_config
763
  candidates = entities.get(name_config_inexistent, default)[:count]
764

    
765
  if len(candidates) < count:
766
    raise Exception("At least %s non-existent %s are needed" %
767
                    (count, name_config))
768

    
769
  return candidates
770

    
771

    
772
def MakeNodePath(node, path):
773
  """Builds an absolute path for a virtual node.
774

775
  @type node: string or L{qa_config._QaNode}
776
  @param node: Node
777
  @type path: string
778
  @param path: Path without node-specific prefix
779

780
  """
781
  (_, basedir) = qa_config.GetVclusterSettings()
782

    
783
  if isinstance(node, basestring):
784
    name = node
785
  else:
786
    name = node.primary
787

    
788
  if basedir:
789
    assert path.startswith("/")
790
    return "%s%s" % (vcluster.MakeNodeRoot(basedir, name), path)
791
  else:
792
    return path
793

    
794

    
795
def _GetParameterOptions(specs):
796
  """Helper to build policy options."""
797
  values = ["%s=%s" % (par, val)
798
            for (par, val) in specs.items()]
799
  return ",".join(values)
800

    
801

    
802
def TestSetISpecs(new_specs=None, diff_specs=None, get_policy_fn=None,
803
                  build_cmd_fn=None, fail=False, old_values=None):
804
  """Change instance specs for an object.
805

806
  At most one of new_specs or diff_specs can be specified.
807

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

828
  """
829
  assert get_policy_fn is not None
830
  assert build_cmd_fn is not None
831
  assert new_specs is None or diff_specs is None
832

    
833
  if old_values:
834
    (old_policy, old_specs) = old_values
835
  else:
836
    (old_policy, old_specs) = get_policy_fn()
837

    
838
  if diff_specs:
839
    new_specs = copy.deepcopy(old_specs)
840
    if constants.ISPECS_MINMAX in diff_specs:
841
      AssertEqual(len(new_specs[constants.ISPECS_MINMAX]),
842
                  len(diff_specs[constants.ISPECS_MINMAX]))
843
      for (new_minmax, diff_minmax) in zip(new_specs[constants.ISPECS_MINMAX],
844
                                           diff_specs[constants.ISPECS_MINMAX]):
845
        for (key, parvals) in diff_minmax.items():
846
          for (par, val) in parvals.items():
847
            new_minmax[key][par] = val
848
    for (par, val) in diff_specs.get(constants.ISPECS_STD, {}).items():
849
      new_specs[constants.ISPECS_STD][par] = val
850

    
851
  if new_specs:
852
    cmd = []
853
    if (diff_specs is None or constants.ISPECS_MINMAX in diff_specs):
854
      minmax_opt_items = []
855
      for minmax in new_specs[constants.ISPECS_MINMAX]:
856
        minmax_opts = []
857
        for key in ["min", "max"]:
858
          keyopt = _GetParameterOptions(minmax[key])
859
          minmax_opts.append("%s:%s" % (key, keyopt))
860
        minmax_opt_items.append("/".join(minmax_opts))
861
      cmd.extend([
862
        "--ipolicy-bounds-specs",
863
        "//".join(minmax_opt_items)
864
        ])
865
    if diff_specs is None:
866
      std_source = new_specs
867
    else:
868
      std_source = diff_specs
869
    std_opt = _GetParameterOptions(std_source.get("std", {}))
870
    if std_opt:
871
      cmd.extend(["--ipolicy-std-specs", std_opt])
872
    AssertCommand(build_cmd_fn(cmd), fail=fail)
873

    
874
    # Check the new state
875
    (eff_policy, eff_specs) = get_policy_fn()
876
    AssertEqual(eff_policy, old_policy)
877
    if fail:
878
      AssertEqual(eff_specs, old_specs)
879
    else:
880
      AssertEqual(eff_specs, new_specs)
881

    
882
  else:
883
    (eff_policy, eff_specs) = (old_policy, old_specs)
884

    
885
  return (eff_policy, eff_specs)
886

    
887

    
888
def ParseIPolicy(policy):
889
  """Parse and split instance an instance policy.
890

891
  @type policy: dict
892
  @param policy: policy, as returned by L{GetObjectInfo}
893
  @rtype: tuple
894
  @return: (policy, specs), where:
895
      - policy is a dictionary of the policy values, instance specs excluded
896
      - specs is a dictionary containing only the specs, using the internal
897
        format (see L{constants.IPOLICY_DEFAULTS} for an example)
898

899
  """
900
  ret_specs = {}
901
  ret_policy = {}
902
  for (key, val) in policy.items():
903
    if key == "bounds specs":
904
      ret_specs[constants.ISPECS_MINMAX] = []
905
      for minmax in val:
906
        ret_minmax = {}
907
        for key in minmax:
908
          keyparts = key.split("/", 1)
909
          assert len(keyparts) > 1
910
          ret_minmax[keyparts[0]] = minmax[key]
911
        ret_specs[constants.ISPECS_MINMAX].append(ret_minmax)
912
    elif key == constants.ISPECS_STD:
913
      ret_specs[key] = val
914
    else:
915
      ret_policy[key] = val
916
  return (ret_policy, ret_specs)
917

    
918

    
919
def UsesIPv6Connection(host, port):
920
  """Returns True if the connection to a given host/port could go through IPv6.
921

922
  """
923
  return any(t[0] == socket.AF_INET6 for t in socket.getaddrinfo(host, port))