Statistics
| Branch: | Tag: | Revision:

root / qa / qa_utils.py @ ffafdcf6

History | View | Annotate | Download (26.2 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, msg=""):
111
  """Raises an error when item is not in sequence.
112

113
  """
114
  if item not in sequence:
115
    if msg:
116
      raise qa_error.Error("%s: %r not in %r" % (msg, item, sequence))
117
    else:
118
      raise qa_error.Error("%r not in %r" % (item, sequence))
119

    
120

    
121
def AssertNotIn(item, sequence):
122
  """Raises an error when item is in sequence.
123

124
  """
125
  if item in sequence:
126
    raise qa_error.Error("%r in %r" % (item, sequence))
127

    
128

    
129
def AssertEqual(first, second, msg=""):
130
  """Raises an error when values aren't equal.
131

132
  """
133
  if not first == second:
134
    if msg:
135
      raise qa_error.Error("%s: %r == %r" % (msg, first, second))
136
    else:
137
      raise qa_error.Error("%r == %r" % (first, second))
138

    
139

    
140
def AssertMatch(string, pattern):
141
  """Raises an error when string doesn't match regexp pattern.
142

143
  """
144
  if not re.match(pattern, string):
145
    raise qa_error.Error("%r doesn't match /%r/" % (string, pattern))
146

    
147

    
148
def _GetName(entity, fn):
149
  """Tries to get name of an entity.
150

151
  @type entity: string or dict
152
  @param fn: Function retrieving name from entity
153

154
  """
155
  if isinstance(entity, basestring):
156
    result = entity
157
  else:
158
    result = fn(entity)
159

    
160
  if not ht.TNonEmptyString(result):
161
    raise Exception("Invalid name '%s'" % result)
162

    
163
  return result
164

    
165

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

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

    
177

    
178
def AssertCommand(cmd, fail=False, node=None, log_cmd=True):
179
  """Checks that a remote command succeeds.
180

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

193
  """
194
  if node is None:
195
    node = qa_config.GetMasterNode()
196

    
197
  nodename = _GetName(node, operator.attrgetter("primary"))
198

    
199
  if isinstance(cmd, basestring):
200
    cmdstr = cmd
201
  else:
202
    cmdstr = utils.ShellQuoteArgs(cmd)
203

    
204
  rcode = StartSSH(nodename, cmdstr, log_cmd=log_cmd).wait()
205
  _AssertRetCode(rcode, fail, cmdstr, nodename)
206

    
207
  return rcode
208

    
209

    
210
def AssertRedirectedCommand(cmd, fail=False, node=None, log_cmd=True):
211
  """Executes a command with redirected output.
212

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

217
  @param cmd: the command to be executed, as a list; a string is not
218
      supported
219

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

    
230

    
231
def GetSSHCommand(node, cmd, strict=True, opts=None, tty=None,
232
                  use_multiplexer=True):
233
  """Builds SSH command to be executed.
234

235
  @type node: string
236
  @param node: node the command should run on
237
  @type cmd: string
238
  @param cmd: command to be executed in the node; if None or empty
239
      string, no command will be executed
240
  @type strict: boolean
241
  @param strict: whether to enable strict host key checking
242
  @type opts: list
243
  @param opts: list of additional options
244
  @type tty: boolean or None
245
  @param tty: if we should use tty; if None, will be auto-detected
246
  @type use_multiplexer: boolean
247
  @param use_multiplexer: if the multiplexer for the node should be used
248

249
  """
250
  args = ["ssh", "-oEscapeChar=none", "-oBatchMode=yes", "-lroot"]
251

    
252
  if tty is None:
253
    tty = sys.stdout.isatty()
254

    
255
  if tty:
256
    args.append("-t")
257

    
258
  if strict:
259
    tmp = "yes"
260
  else:
261
    tmp = "no"
262
  args.append("-oStrictHostKeyChecking=%s" % tmp)
263
  args.append("-oClearAllForwardings=yes")
264
  args.append("-oForwardAgent=yes")
265
  if opts:
266
    args.extend(opts)
267
  if node in _MULTIPLEXERS and use_multiplexer:
268
    spath = _MULTIPLEXERS[node][0]
269
    args.append("-oControlPath=%s" % spath)
270
    args.append("-oControlMaster=no")
271

    
272
  (vcluster_master, vcluster_basedir) = \
273
    qa_config.GetVclusterSettings()
274

    
275
  if vcluster_master:
276
    args.append(vcluster_master)
277
    args.append("%s/%s/cmd" % (vcluster_basedir, node))
278

    
279
    if cmd:
280
      # For virtual clusters the whole command must be wrapped using the "cmd"
281
      # script, as that script sets a number of environment variables. If the
282
      # command contains shell meta characters the whole command needs to be
283
      # quoted.
284
      args.append(utils.ShellQuote(cmd))
285
  else:
286
    args.append(node)
287

    
288
    if cmd:
289
      args.append(cmd)
290

    
291
  return args
292

    
293

    
294
def StartLocalCommand(cmd, _nolog_opts=False, log_cmd=True, **kwargs):
295
  """Starts a local command.
296

297
  """
298
  if log_cmd:
299
    if _nolog_opts:
300
      pcmd = [i for i in cmd if not i.startswith("-")]
301
    else:
302
      pcmd = cmd
303
    print "%s %s" % (colors.colorize("Command:", colors.CYAN),
304
                     utils.ShellQuoteArgs(pcmd))
305
  return subprocess.Popen(cmd, shell=False, **kwargs)
306

    
307

    
308
def StartSSH(node, cmd, strict=True, log_cmd=True):
309
  """Starts SSH.
310

311
  """
312
  return StartLocalCommand(GetSSHCommand(node, cmd, strict=strict),
313
                           _nolog_opts=True, log_cmd=log_cmd)
314

    
315

    
316
def StartMultiplexer(node):
317
  """Starts a multiplexer command.
318

319
  @param node: the node for which to open the multiplexer
320

321
  """
322
  if node in _MULTIPLEXERS:
323
    return
324

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

    
333

    
334
def CloseMultiplexers():
335
  """Closes all current multiplexers and cleans up.
336

337
  """
338
  for node in _MULTIPLEXERS.keys():
339
    (sname, child) = _MULTIPLEXERS.pop(node)
340
    utils.KillProcess(child.pid, timeout=10, waitpid=True)
341
    utils.RemoveFile(sname)
342

    
343

    
344
def GetCommandOutput(node, cmd, tty=None, use_multiplexer=True, log_cmd=True,
345
                     fail=False):
346
  """Returns the output of a command executed on the given node.
347

348
  @type node: string
349
  @param node: node the command should run on
350
  @type cmd: string
351
  @param cmd: command to be executed in the node (cannot be empty or None)
352
  @type tty: bool or None
353
  @param tty: if we should use tty; if None, it will be auto-detected
354
  @type use_multiplexer: bool
355
  @param use_multiplexer: if the SSH multiplexer provided by the QA should be
356
                          used or not
357
  @type log_cmd: bool
358
  @param log_cmd: if the command should be logged
359
  @type fail: bool
360
  @param fail: whether the command is expected to fail
361
  """
362
  assert cmd
363
  p = StartLocalCommand(GetSSHCommand(node, cmd, tty=tty,
364
                                      use_multiplexer=use_multiplexer),
365
                        stdout=subprocess.PIPE, log_cmd=log_cmd)
366
  rcode = p.wait()
367
  _AssertRetCode(rcode, fail, cmd, node)
368
  return p.stdout.read()
369

    
370

    
371
def GetObjectInfo(infocmd):
372
  """Get and parse information about a Ganeti object.
373

374
  @type infocmd: list of strings
375
  @param infocmd: command to be executed, e.g. ["gnt-cluster", "info"]
376
  @return: the information parsed, appropriately stored in dictionaries,
377
      lists...
378

379
  """
380
  master = qa_config.GetMasterNode()
381
  cmdline = utils.ShellQuoteArgs(infocmd)
382
  info_out = GetCommandOutput(master.primary, cmdline)
383
  return yaml.load(info_out)
384

    
385

    
386
def UploadFile(node, src):
387
  """Uploads a file to a node and returns the filename.
388

389
  Caller needs to remove the returned file on the node when it's not needed
390
  anymore.
391

392
  """
393
  # Make sure nobody else has access to it while preserving local permissions
394
  mode = os.stat(src).st_mode & 0700
395

    
396
  cmd = ('tmp=$(mktemp --tmpdir gnt.XXXXXX) && '
397
         'chmod %o "${tmp}" && '
398
         '[[ -f "${tmp}" ]] && '
399
         'cat > "${tmp}" && '
400
         'echo "${tmp}"') % mode
401

    
402
  f = open(src, "r")
403
  try:
404
    p = subprocess.Popen(GetSSHCommand(node, cmd), shell=False, stdin=f,
405
                         stdout=subprocess.PIPE)
406
    AssertEqual(p.wait(), 0)
407

    
408
    # Return temporary filename
409
    return p.stdout.read().strip()
410
  finally:
411
    f.close()
412

    
413

    
414
def UploadData(node, data, mode=0600, filename=None):
415
  """Uploads data to a node and returns the filename.
416

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

420
  """
421
  if filename:
422
    tmp = "tmp=%s" % utils.ShellQuote(filename)
423
  else:
424
    tmp = ('tmp=$(mktemp --tmpdir gnt.XXXXXX) && '
425
           'chmod %o "${tmp}"') % mode
426
  cmd = ("%s && "
427
         "[[ -f \"${tmp}\" ]] && "
428
         "cat > \"${tmp}\" && "
429
         "echo \"${tmp}\"") % tmp
430

    
431
  p = subprocess.Popen(GetSSHCommand(node, cmd), shell=False,
432
                       stdin=subprocess.PIPE, stdout=subprocess.PIPE)
433
  p.stdin.write(data)
434
  p.stdin.close()
435
  AssertEqual(p.wait(), 0)
436

    
437
  # Return temporary filename
438
  return p.stdout.read().strip()
439

    
440

    
441
def BackupFile(node, path):
442
  """Creates a backup of a file on the node and returns the filename.
443

444
  Caller needs to remove the returned file on the node when it's not needed
445
  anymore.
446

447
  """
448
  vpath = MakeNodePath(node, path)
449

    
450
  cmd = ("tmp=$(mktemp .gnt.XXXXXX --tmpdir=$(dirname %s)) && "
451
         "[[ -f \"$tmp\" ]] && "
452
         "cp %s $tmp && "
453
         "echo $tmp") % (utils.ShellQuote(vpath), utils.ShellQuote(vpath))
454

    
455
  # Return temporary filename
456
  result = GetCommandOutput(node, cmd).strip()
457

    
458
  print "Backup filename: %s" % result
459

    
460
  return result
461

    
462

    
463
def ResolveInstanceName(instance):
464
  """Gets the full name of an instance.
465

466
  @type instance: string
467
  @param instance: Instance name
468

469
  """
470
  info = GetObjectInfo(["gnt-instance", "info", instance])
471
  return info[0]["Instance name"]
472

    
473

    
474
def ResolveNodeName(node):
475
  """Gets the full name of a node.
476

477
  """
478
  info = GetObjectInfo(["gnt-node", "info", node.primary])
479
  return info[0]["Node name"]
480

    
481

    
482
def GetNodeInstances(node, secondaries=False):
483
  """Gets a list of instances on a node.
484

485
  """
486
  master = qa_config.GetMasterNode()
487
  node_name = ResolveNodeName(node)
488

    
489
  # Get list of all instances
490
  cmd = ["gnt-instance", "list", "--separator=:", "--no-headers",
491
         "--output=name,pnode,snodes"]
492
  output = GetCommandOutput(master.primary, utils.ShellQuoteArgs(cmd))
493

    
494
  instances = []
495
  for line in output.splitlines():
496
    (name, pnode, snodes) = line.split(":", 2)
497
    if ((not secondaries and pnode == node_name) or
498
        (secondaries and node_name in snodes.split(","))):
499
      instances.append(name)
500

    
501
  return instances
502

    
503

    
504
def _SelectQueryFields(rnd, fields):
505
  """Generates a list of fields for query tests.
506

507
  """
508
  # Create copy for shuffling
509
  fields = list(fields)
510
  rnd.shuffle(fields)
511

    
512
  # Check all fields
513
  yield fields
514
  yield sorted(fields)
515

    
516
  # Duplicate fields
517
  yield fields + fields
518

    
519
  # Check small groups of fields
520
  while fields:
521
    yield [fields.pop() for _ in range(rnd.randint(2, 10)) if fields]
522

    
523

    
524
def _List(listcmd, fields, names):
525
  """Runs a list command.
526

527
  """
528
  master = qa_config.GetMasterNode()
529

    
530
  cmd = [listcmd, "list", "--separator=|", "--no-headers",
531
         "--output", ",".join(fields)]
532

    
533
  if names:
534
    cmd.extend(names)
535

    
536
  return GetCommandOutput(master.primary,
537
                          utils.ShellQuoteArgs(cmd)).splitlines()
538

    
539

    
540
def GenericQueryTest(cmd, fields, namefield="name", test_unknown=True):
541
  """Runs a number of tests on query commands.
542

543
  @param cmd: Command name
544
  @param fields: List of field names
545

546
  """
547
  rnd = random.Random(hash(cmd))
548

    
549
  fields = list(fields)
550
  rnd.shuffle(fields)
551

    
552
  # Test a number of field combinations
553
  for testfields in _SelectQueryFields(rnd, fields):
554
    AssertRedirectedCommand([cmd, "list", "--output", ",".join(testfields)])
555

    
556
  if namefield is not None:
557
    namelist_fn = compat.partial(_List, cmd, [namefield])
558

    
559
    # When no names were requested, the list must be sorted
560
    names = namelist_fn(None)
561
    AssertEqual(names, utils.NiceSort(names))
562

    
563
    # When requesting specific names, the order must be kept
564
    revnames = list(reversed(names))
565
    AssertEqual(namelist_fn(revnames), revnames)
566

    
567
    randnames = list(names)
568
    rnd.shuffle(randnames)
569
    AssertEqual(namelist_fn(randnames), randnames)
570

    
571
  if test_unknown:
572
    # Listing unknown items must fail
573
    AssertCommand([cmd, "list", "this.name.certainly.does.not.exist"],
574
                  fail=True)
575

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

    
582

    
583
def GenericQueryFieldsTest(cmd, fields):
584
  master = qa_config.GetMasterNode()
585

    
586
  # Listing fields
587
  AssertRedirectedCommand([cmd, "list-fields"])
588
  AssertRedirectedCommand([cmd, "list-fields"] + fields)
589

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

    
597
  # Check exit code for listing unknown field
598
  AssertEqual(AssertCommand([cmd, "list-fields", "field/does/not/exist"],
599
                            fail=True),
600
              constants.EXIT_UNKNOWN_FIELD)
601

    
602

    
603
def _FormatWithColor(text, seq):
604
  if not seq:
605
    return text
606
  return "%s%s%s" % (seq, text, _RESET_SEQ)
607

    
608

    
609
FormatWarning = lambda text: _FormatWithColor(text, _WARNING_SEQ)
610
FormatError = lambda text: _FormatWithColor(text, _ERROR_SEQ)
611
FormatInfo = lambda text: _FormatWithColor(text, _INFO_SEQ)
612

    
613

    
614
def AddToEtcHosts(hostnames):
615
  """Adds hostnames to /etc/hosts.
616

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

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

    
623
  data = []
624
  for localhost in ("::1", "127.0.0.1"):
625
    data.append("%s %s" % (localhost, " ".join(hostnames)))
626

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

    
638

    
639
def RemoveFromEtcHosts(hostnames):
640
  """Remove hostnames from /etc/hosts.
641

642
  @param hostnames: List of hostnames first used A records, all other CNAMEs
643

644
  """
645
  master = qa_config.GetMasterNode()
646
  tmp_hosts = UploadData(master.primary, "", mode=0644)
647
  quoted_tmp_hosts = utils.ShellQuote(tmp_hosts)
648

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

    
660

    
661
def RunInstanceCheck(instance, running):
662
  """Check if instance is running or not.
663

664
  """
665
  instance_name = _GetName(instance, operator.attrgetter("name"))
666

    
667
  script = qa_config.GetInstanceCheckScript()
668
  if not script:
669
    return
670

    
671
  master_node = qa_config.GetMasterNode()
672

    
673
  # Build command to connect to master node
674
  master_ssh = GetSSHCommand(master_node.primary, "--")
675

    
676
  if running:
677
    running_shellval = "1"
678
    running_text = ""
679
  else:
680
    running_shellval = ""
681
    running_text = "not "
682

    
683
  print FormatInfo("Checking if instance '%s' is %srunning" %
684
                   (instance_name, running_text))
685

    
686
  args = [script, instance_name]
687
  env = {
688
    "PATH": constants.HOOKS_PATH,
689
    "RUN_UUID": _RUN_UUID,
690
    "MASTER_SSH": utils.ShellQuoteArgs(master_ssh),
691
    "INSTANCE_NAME": instance_name,
692
    "INSTANCE_RUNNING": running_shellval,
693
    }
694

    
695
  result = os.spawnve(os.P_WAIT, script, args, env)
696
  if result != 0:
697
    raise qa_error.Error("Instance check failed with result %s" % result)
698

    
699

    
700
def _InstanceCheckInner(expected, instarg, args, result):
701
  """Helper function used by L{InstanceCheck}.
702

703
  """
704
  if instarg == FIRST_ARG:
705
    instance = args[0]
706
  elif instarg == RETURN_VALUE:
707
    instance = result
708
  else:
709
    raise Exception("Invalid value '%s' for instance argument" % instarg)
710

    
711
  if expected in (INST_DOWN, INST_UP):
712
    RunInstanceCheck(instance, (expected == INST_UP))
713
  elif expected is not None:
714
    raise Exception("Invalid value '%s'" % expected)
715

    
716

    
717
def InstanceCheck(before, after, instarg):
718
  """Decorator to check instance status before and after test.
719

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

727
  """
728
  def decorator(fn):
729
    @functools.wraps(fn)
730
    def wrapper(*args, **kwargs):
731
      _InstanceCheckInner(before, instarg, args, NotImplemented)
732

    
733
      result = fn(*args, **kwargs)
734

    
735
      _InstanceCheckInner(after, instarg, args, result)
736

    
737
      return result
738
    return wrapper
739
  return decorator
740

    
741

    
742
def GetNonexistentGroups(count):
743
  """Gets group names which shouldn't exist on the cluster.
744

745
  @param count: Number of groups to get
746
  @rtype: integer
747

748
  """
749
  return GetNonexistentEntityNames(count, "groups", "group")
750

    
751

    
752
def GetNonexistentEntityNames(count, name_config, name_prefix):
753
  """Gets entity names which shouldn't exist on the cluster.
754

755
  The actualy names can refer to arbitrary entities (for example
756
  groups, networks).
757

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

769
  """
770
  entities = qa_config.get(name_config, {})
771

    
772
  default = [name_prefix + str(i) for i in range(count)]
773
  assert count <= len(default)
774

    
775
  name_config_inexistent = "inexistent-" + name_config
776
  candidates = entities.get(name_config_inexistent, default)[:count]
777

    
778
  if len(candidates) < count:
779
    raise Exception("At least %s non-existent %s are needed" %
780
                    (count, name_config))
781

    
782
  return candidates
783

    
784

    
785
def MakeNodePath(node, path):
786
  """Builds an absolute path for a virtual node.
787

788
  @type node: string or L{qa_config._QaNode}
789
  @param node: Node
790
  @type path: string
791
  @param path: Path without node-specific prefix
792

793
  """
794
  (_, basedir) = qa_config.GetVclusterSettings()
795

    
796
  if isinstance(node, basestring):
797
    name = node
798
  else:
799
    name = node.primary
800

    
801
  if basedir:
802
    assert path.startswith("/")
803
    return "%s%s" % (vcluster.MakeNodeRoot(basedir, name), path)
804
  else:
805
    return path
806

    
807

    
808
def _GetParameterOptions(specs):
809
  """Helper to build policy options."""
810
  values = ["%s=%s" % (par, val)
811
            for (par, val) in specs.items()]
812
  return ",".join(values)
813

    
814

    
815
def TestSetISpecs(new_specs=None, diff_specs=None, get_policy_fn=None,
816
                  build_cmd_fn=None, fail=False, old_values=None):
817
  """Change instance specs for an object.
818

819
  At most one of new_specs or diff_specs can be specified.
820

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

841
  """
842
  assert get_policy_fn is not None
843
  assert build_cmd_fn is not None
844
  assert new_specs is None or diff_specs is None
845

    
846
  if old_values:
847
    (old_policy, old_specs) = old_values
848
  else:
849
    (old_policy, old_specs) = get_policy_fn()
850

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

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

    
887
    # Check the new state
888
    (eff_policy, eff_specs) = get_policy_fn()
889
    AssertEqual(eff_policy, old_policy)
890
    if fail:
891
      AssertEqual(eff_specs, old_specs)
892
    else:
893
      AssertEqual(eff_specs, new_specs)
894

    
895
  else:
896
    (eff_policy, eff_specs) = (old_policy, old_specs)
897

    
898
  return (eff_policy, eff_specs)
899

    
900

    
901
def ParseIPolicy(policy):
902
  """Parse and split instance an instance policy.
903

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

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

    
931

    
932
def UsesIPv6Connection(host, port):
933
  """Returns True if the connection to a given host/port could go through IPv6.
934

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