Statistics
| Branch: | Tag: | Revision:

root / qa / qa_utils.py @ a02dbfca

History | View | Annotate | Download (25.3 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
  curses.setupterm()
94

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

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

    
102

    
103
_SetupColours()
104

    
105

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

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

    
113

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

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

    
121

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

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

    
129

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

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

    
137

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

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

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

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

    
153
  return result
154

    
155

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

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

    
167

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

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

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

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

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

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

    
197
  return rcode
198

    
199

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

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

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

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

    
220

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

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

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

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

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

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

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

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

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

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

    
278
  return args
279

    
280

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

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

    
294

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

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

    
302

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

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

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

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

    
320

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

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

    
330

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

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

    
350

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

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

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

    
365

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

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

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

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

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

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

    
393

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

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

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

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

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

    
420

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

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

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

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

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

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

    
440
  return result
441

    
442

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

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

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

    
453

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

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

    
461

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

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

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

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

    
481
  return instances
482

    
483

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

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

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

    
496
  # Duplicate fields
497
  yield fields + fields
498

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

    
503

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

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

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

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

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

    
519

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

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

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

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

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

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

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

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

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

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

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

    
562

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

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

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

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

    
582

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

    
588

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

    
593

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

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

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

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

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

    
618

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

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

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

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

    
640

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

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

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

    
651
  master_node = qa_config.GetMasterNode()
652

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

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

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

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

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

    
679

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

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

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

    
696

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

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

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

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

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

    
717
      return result
718
    return wrapper
719
  return decorator
720

    
721

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

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

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

    
731

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

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

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

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

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

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

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

    
762
  return candidates
763

    
764

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

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

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

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

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

    
787

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

    
794

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

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

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

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

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

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

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

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

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

    
878
  return (eff_policy, eff_specs)
879

    
880

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

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

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

    
911

    
912
def UsesIPv6Connection(host, port):
913
  """Returns True if the connection to a given host/port could go through IPv6.
914

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