Statistics
| Branch: | Tag: | Revision:

root / qa / qa_utils.py @ 82ce55fa

History | View | Annotate | Download (25.4 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):
127
  """Raises an error when values aren't equal.
128

129
  """
130
  if not first == second:
131
    raise qa_error.Error("%r == %r" % (first, second))
132

    
133

    
134
def AssertMatch(string, pattern):
135
  """Raises an error when string doesn't match regexp pattern.
136

137
  """
138
  if not re.match(pattern, string):
139
    raise qa_error.Error("%r doesn't match /%r/" % (string, pattern))
140

    
141

    
142
def _GetName(entity, fn):
143
  """Tries to get name of an entity.
144

145
  @type entity: string or dict
146
  @param fn: Function retrieving name from entity
147

148
  """
149
  if isinstance(entity, basestring):
150
    result = entity
151
  else:
152
    result = fn(entity)
153

    
154
  if not ht.TNonEmptyString(result):
155
    raise Exception("Invalid name '%s'" % result)
156

    
157
  return result
158

    
159

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

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

    
171

    
172
def AssertCommand(cmd, fail=False, node=None, log_cmd=True):
173
  """Checks that a remote command succeeds.
174

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

187
  """
188
  if node is None:
189
    node = qa_config.GetMasterNode()
190

    
191
  nodename = _GetName(node, operator.attrgetter("primary"))
192

    
193
  if isinstance(cmd, basestring):
194
    cmdstr = cmd
195
  else:
196
    cmdstr = utils.ShellQuoteArgs(cmd)
197

    
198
  rcode = StartSSH(nodename, cmdstr, log_cmd=log_cmd).wait()
199
  _AssertRetCode(rcode, fail, cmdstr, nodename)
200

    
201
  return rcode
202

    
203

    
204
def AssertRedirectedCommand(cmd, fail=False, node=None, log_cmd=True):
205
  """Executes a command with redirected output.
206

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

211
  @param cmd: the command to be executed, as a list; a string is not
212
      supported
213

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

    
224

    
225
def GetSSHCommand(node, cmd, strict=True, opts=None, tty=None):
226
  """Builds SSH command to be executed.
227

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

240
  """
241
  args = ["ssh", "-oEscapeChar=none", "-oBatchMode=yes", "-lroot"]
242

    
243
  if tty is None:
244
    tty = sys.stdout.isatty()
245

    
246
  if tty:
247
    args.append("-t")
248

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

    
263
  (vcluster_master, vcluster_basedir) = \
264
    qa_config.GetVclusterSettings()
265

    
266
  if vcluster_master:
267
    args.append(vcluster_master)
268
    args.append("%s/%s/cmd" % (vcluster_basedir, node))
269

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

    
279
    if cmd:
280
      args.append(cmd)
281

    
282
  return args
283

    
284

    
285
def StartLocalCommand(cmd, _nolog_opts=False, log_cmd=True, **kwargs):
286
  """Starts a local command.
287

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

    
298

    
299
def StartSSH(node, cmd, strict=True, log_cmd=True):
300
  """Starts SSH.
301

302
  """
303
  return StartLocalCommand(GetSSHCommand(node, cmd, strict=strict),
304
                           _nolog_opts=True, log_cmd=log_cmd)
305

    
306

    
307
def StartMultiplexer(node):
308
  """Starts a multiplexer command.
309

310
  @param node: the node for which to open the multiplexer
311

312
  """
313
  if node in _MULTIPLEXERS:
314
    return
315

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

    
324

    
325
def CloseMultiplexers():
326
  """Closes all current multiplexers and cleans up.
327

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

    
334

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

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

    
354

    
355
def GetObjectInfo(infocmd):
356
  """Get and parse information about a Ganeti object.
357

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

363
  """
364
  master = qa_config.GetMasterNode()
365
  cmdline = utils.ShellQuoteArgs(infocmd)
366
  info_out = GetCommandOutput(master.primary, cmdline)
367
  return yaml.load(info_out)
368

    
369

    
370
def UploadFile(node, src):
371
  """Uploads a file to a node and returns the filename.
372

373
  Caller needs to remove the returned file on the node when it's not needed
374
  anymore.
375

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

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

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

    
392
    # Return temporary filename
393
    return p.stdout.read().strip()
394
  finally:
395
    f.close()
396

    
397

    
398
def UploadData(node, data, mode=0600, filename=None):
399
  """Uploads data to a node and returns the filename.
400

401
  Caller needs to remove the returned file on the node when it's not needed
402
  anymore.
403

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

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

    
421
  # Return temporary filename
422
  return p.stdout.read().strip()
423

    
424

    
425
def BackupFile(node, path):
426
  """Creates a backup of a file on the node and returns the filename.
427

428
  Caller needs to remove the returned file on the node when it's not needed
429
  anymore.
430

431
  """
432
  vpath = MakeNodePath(node, path)
433

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

    
439
  # Return temporary filename
440
  result = GetCommandOutput(node, cmd).strip()
441

    
442
  print "Backup filename: %s" % result
443

    
444
  return result
445

    
446

    
447
def ResolveInstanceName(instance):
448
  """Gets the full name of an instance.
449

450
  @type instance: string
451
  @param instance: Instance name
452

453
  """
454
  info = GetObjectInfo(["gnt-instance", "info", instance])
455
  return info[0]["Instance name"]
456

    
457

    
458
def ResolveNodeName(node):
459
  """Gets the full name of a node.
460

461
  """
462
  info = GetObjectInfo(["gnt-node", "info", node.primary])
463
  return info[0]["Node name"]
464

    
465

    
466
def GetNodeInstances(node, secondaries=False):
467
  """Gets a list of instances on a node.
468

469
  """
470
  master = qa_config.GetMasterNode()
471
  node_name = ResolveNodeName(node)
472

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

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

    
485
  return instances
486

    
487

    
488
def _SelectQueryFields(rnd, fields):
489
  """Generates a list of fields for query tests.
490

491
  """
492
  # Create copy for shuffling
493
  fields = list(fields)
494
  rnd.shuffle(fields)
495

    
496
  # Check all fields
497
  yield fields
498
  yield sorted(fields)
499

    
500
  # Duplicate fields
501
  yield fields + fields
502

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

    
507

    
508
def _List(listcmd, fields, names):
509
  """Runs a list command.
510

511
  """
512
  master = qa_config.GetMasterNode()
513

    
514
  cmd = [listcmd, "list", "--separator=|", "--no-headers",
515
         "--output", ",".join(fields)]
516

    
517
  if names:
518
    cmd.extend(names)
519

    
520
  return GetCommandOutput(master.primary,
521
                          utils.ShellQuoteArgs(cmd)).splitlines()
522

    
523

    
524
def GenericQueryTest(cmd, fields, namefield="name", test_unknown=True):
525
  """Runs a number of tests on query commands.
526

527
  @param cmd: Command name
528
  @param fields: List of field names
529

530
  """
531
  rnd = random.Random(hash(cmd))
532

    
533
  fields = list(fields)
534
  rnd.shuffle(fields)
535

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

    
540
  if namefield is not None:
541
    namelist_fn = compat.partial(_List, cmd, [namefield])
542

    
543
    # When no names were requested, the list must be sorted
544
    names = namelist_fn(None)
545
    AssertEqual(names, utils.NiceSort(names))
546

    
547
    # When requesting specific names, the order must be kept
548
    revnames = list(reversed(names))
549
    AssertEqual(namelist_fn(revnames), revnames)
550

    
551
    randnames = list(names)
552
    rnd.shuffle(randnames)
553
    AssertEqual(namelist_fn(randnames), randnames)
554

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

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

    
566

    
567
def GenericQueryFieldsTest(cmd, fields):
568
  master = qa_config.GetMasterNode()
569

    
570
  # Listing fields
571
  AssertRedirectedCommand([cmd, "list-fields"])
572
  AssertRedirectedCommand([cmd, "list-fields"] + fields)
573

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

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

    
586

    
587
def _FormatWithColor(text, seq):
588
  if not seq:
589
    return text
590
  return "%s%s%s" % (seq, text, _RESET_SEQ)
591

    
592

    
593
FormatWarning = lambda text: _FormatWithColor(text, _WARNING_SEQ)
594
FormatError = lambda text: _FormatWithColor(text, _ERROR_SEQ)
595
FormatInfo = lambda text: _FormatWithColor(text, _INFO_SEQ)
596

    
597

    
598
def AddToEtcHosts(hostnames):
599
  """Adds hostnames to /etc/hosts.
600

601
  @param hostnames: List of hostnames first used A records, all other CNAMEs
602

603
  """
604
  master = qa_config.GetMasterNode()
605
  tmp_hosts = UploadData(master.primary, "", mode=0644)
606

    
607
  data = []
608
  for localhost in ("::1", "127.0.0.1"):
609
    data.append("%s %s" % (localhost, " ".join(hostnames)))
610

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

    
622

    
623
def RemoveFromEtcHosts(hostnames):
624
  """Remove hostnames from /etc/hosts.
625

626
  @param hostnames: List of hostnames first used A records, all other CNAMEs
627

628
  """
629
  master = qa_config.GetMasterNode()
630
  tmp_hosts = UploadData(master.primary, "", mode=0644)
631
  quoted_tmp_hosts = utils.ShellQuote(tmp_hosts)
632

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

    
644

    
645
def RunInstanceCheck(instance, running):
646
  """Check if instance is running or not.
647

648
  """
649
  instance_name = _GetName(instance, operator.attrgetter("name"))
650

    
651
  script = qa_config.GetInstanceCheckScript()
652
  if not script:
653
    return
654

    
655
  master_node = qa_config.GetMasterNode()
656

    
657
  # Build command to connect to master node
658
  master_ssh = GetSSHCommand(master_node.primary, "--")
659

    
660
  if running:
661
    running_shellval = "1"
662
    running_text = ""
663
  else:
664
    running_shellval = ""
665
    running_text = "not "
666

    
667
  print FormatInfo("Checking if instance '%s' is %srunning" %
668
                   (instance_name, running_text))
669

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

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

    
683

    
684
def _InstanceCheckInner(expected, instarg, args, result):
685
  """Helper function used by L{InstanceCheck}.
686

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

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

    
700

    
701
def InstanceCheck(before, after, instarg):
702
  """Decorator to check instance status before and after test.
703

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

711
  """
712
  def decorator(fn):
713
    @functools.wraps(fn)
714
    def wrapper(*args, **kwargs):
715
      _InstanceCheckInner(before, instarg, args, NotImplemented)
716

    
717
      result = fn(*args, **kwargs)
718

    
719
      _InstanceCheckInner(after, instarg, args, result)
720

    
721
      return result
722
    return wrapper
723
  return decorator
724

    
725

    
726
def GetNonexistentGroups(count):
727
  """Gets group names which shouldn't exist on the cluster.
728

729
  @param count: Number of groups to get
730
  @rtype: integer
731

732
  """
733
  return GetNonexistentEntityNames(count, "groups", "group")
734

    
735

    
736
def GetNonexistentEntityNames(count, name_config, name_prefix):
737
  """Gets entity names which shouldn't exist on the cluster.
738

739
  The actualy names can refer to arbitrary entities (for example
740
  groups, networks).
741

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

753
  """
754
  entities = qa_config.get(name_config, {})
755

    
756
  default = [name_prefix + str(i) for i in range(count)]
757
  assert count <= len(default)
758

    
759
  name_config_inexistent = "inexistent-" + name_config
760
  candidates = entities.get(name_config_inexistent, default)[:count]
761

    
762
  if len(candidates) < count:
763
    raise Exception("At least %s non-existent %s are needed" %
764
                    (count, name_config))
765

    
766
  return candidates
767

    
768

    
769
def MakeNodePath(node, path):
770
  """Builds an absolute path for a virtual node.
771

772
  @type node: string or L{qa_config._QaNode}
773
  @param node: Node
774
  @type path: string
775
  @param path: Path without node-specific prefix
776

777
  """
778
  (_, basedir) = qa_config.GetVclusterSettings()
779

    
780
  if isinstance(node, basestring):
781
    name = node
782
  else:
783
    name = node.primary
784

    
785
  if basedir:
786
    assert path.startswith("/")
787
    return "%s%s" % (vcluster.MakeNodeRoot(basedir, name), path)
788
  else:
789
    return path
790

    
791

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

    
798

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

803
  At most one of new_specs or diff_specs can be specified.
804

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

825
  """
826
  assert get_policy_fn is not None
827
  assert build_cmd_fn is not None
828
  assert new_specs is None or diff_specs is None
829

    
830
  if old_values:
831
    (old_policy, old_specs) = old_values
832
  else:
833
    (old_policy, old_specs) = get_policy_fn()
834

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

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

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

    
879
  else:
880
    (eff_policy, eff_specs) = (old_policy, old_specs)
881

    
882
  return (eff_policy, eff_specs)
883

    
884

    
885
def ParseIPolicy(policy):
886
  """Parse and split instance an instance policy.
887

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

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

    
915

    
916
def UsesIPv6Connection(host, port):
917
  """Returns True if the connection to a given host/port could go through IPv6.
918

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