Statistics
| Branch: | Tag: | Revision:

root / qa / qa_utils.py @ bfca72bc

History | View | Annotate | Download (24 kB)

1
#
2
#
3

    
4
# Copyright (C) 2007, 2011, 2012, 2013 Google Inc.
5
#
6
# This program is free software; you can redistribute it and/or modify
7
# it under the terms of the GNU General Public License as published by
8
# the Free Software Foundation; either version 2 of the License, or
9
# (at your option) any later version.
10
#
11
# This program is distributed in the hope that it will be useful, but
12
# WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14
# General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License
17
# along with this program; if not, write to the Free Software
18
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19
# 02110-1301, USA.
20

    
21

    
22
"""Utilities for QA tests.
23

24
"""
25

    
26
import copy
27
import operator
28
import os
29
import random
30
import re
31
import subprocess
32
import sys
33
import tempfile
34
import yaml
35

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

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

    
48
import qa_config
49
import qa_error
50

    
51
from qa_logging import FormatInfo
52

    
53

    
54
_MULTIPLEXERS = {}
55

    
56
#: Unique ID per QA run
57
_RUN_UUID = utils.NewUUID()
58

    
59
#: Path to the QA query output log file
60
_QA_OUTPUT = pathutils.GetLogFilename("qa-output")
61

    
62

    
63
(INST_DOWN,
64
 INST_UP) = range(500, 502)
65

    
66
(FIRST_ARG,
67
 RETURN_VALUE) = range(1000, 1002)
68

    
69

    
70
def AssertIn(item, sequence):
71
  """Raises an error when item is not in sequence.
72

73
  """
74
  if item not in sequence:
75
    raise qa_error.Error("%r not in %r" % (item, sequence))
76

    
77

    
78
def AssertNotIn(item, sequence):
79
  """Raises an error when item is in sequence.
80

81
  """
82
  if item in sequence:
83
    raise qa_error.Error("%r in %r" % (item, sequence))
84

    
85

    
86
def AssertEqual(first, second):
87
  """Raises an error when values aren't equal.
88

89
  """
90
  if not first == second:
91
    raise qa_error.Error("%r == %r" % (first, second))
92

    
93

    
94
def AssertMatch(string, pattern):
95
  """Raises an error when string doesn't match regexp pattern.
96

97
  """
98
  if not re.match(pattern, string):
99
    raise qa_error.Error("%r doesn't match /%r/" % (string, pattern))
100

    
101

    
102
def _GetName(entity, fn):
103
  """Tries to get name of an entity.
104

105
  @type entity: string or dict
106
  @param fn: Function retrieving name from entity
107

108
  """
109
  if isinstance(entity, basestring):
110
    result = entity
111
  else:
112
    result = fn(entity)
113

    
114
  if not ht.TNonEmptyString(result):
115
    raise Exception("Invalid name '%s'" % result)
116

    
117
  return result
118

    
119

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

123
  """
124
  if fail and rcode == 0:
125
    raise qa_error.Error("Command '%s' on node %s was expected to fail but"
126
                         " didn't" % (cmdstr, nodename))
127
  elif not fail and rcode != 0:
128
    raise qa_error.Error("Command '%s' on node %s failed, exit code %s" %
129
                         (cmdstr, nodename, rcode))
130

    
131

    
132
def AssertCommand(cmd, fail=False, node=None, log_cmd=True):
133
  """Checks that a remote command succeeds.
134

135
  @param cmd: either a string (the command to execute) or a list (to
136
      be converted using L{utils.ShellQuoteArgs} into a string)
137
  @type fail: boolean
138
  @param fail: if the command is expected to fail instead of succeeding
139
  @param node: if passed, it should be the node on which the command
140
      should be executed, instead of the master node (can be either a
141
      dict or a string)
142
  @param log_cmd: if False, the command won't be logged (simply passed to
143
      StartSSH)
144
  @return: the return code of the command
145
  @raise qa_error.Error: if the command fails when it shouldn't or vice versa
146

147
  """
148
  if node is None:
149
    node = qa_config.GetMasterNode()
150

    
151
  nodename = _GetName(node, operator.attrgetter("primary"))
152

    
153
  if isinstance(cmd, basestring):
154
    cmdstr = cmd
155
  else:
156
    cmdstr = utils.ShellQuoteArgs(cmd)
157

    
158
  rcode = StartSSH(nodename, cmdstr, log_cmd=log_cmd).wait()
159
  _AssertRetCode(rcode, fail, cmdstr, nodename)
160

    
161
  return rcode
162

    
163

    
164
def AssertRedirectedCommand(cmd, fail=False, node=None, log_cmd=True):
165
  """Executes a command with redirected output.
166

167
  The log will go to the qa-output log file in the ganeti log
168
  directory on the node where the command is executed. The fail and
169
  node parameters are passed unchanged to AssertCommand.
170

171
  @param cmd: the command to be executed, as a list; a string is not
172
      supported
173

174
  """
175
  if not isinstance(cmd, list):
176
    raise qa_error.Error("Non-list passed to AssertRedirectedCommand")
177
  ofile = utils.ShellQuote(_QA_OUTPUT)
178
  cmdstr = utils.ShellQuoteArgs(cmd)
179
  AssertCommand("echo ---- $(date) %s ---- >> %s" % (cmdstr, ofile),
180
                fail=False, node=node, log_cmd=False)
181
  return AssertCommand(cmdstr + " >> %s" % ofile,
182
                       fail=fail, node=node, log_cmd=log_cmd)
183

    
184

    
185
def GetSSHCommand(node, cmd, strict=True, opts=None, tty=None):
186
  """Builds SSH command to be executed.
187

188
  @type node: string
189
  @param node: node the command should run on
190
  @type cmd: string
191
  @param cmd: command to be executed in the node; if None or empty
192
      string, no command will be executed
193
  @type strict: boolean
194
  @param strict: whether to enable strict host key checking
195
  @type opts: list
196
  @param opts: list of additional options
197
  @type tty: boolean or None
198
  @param tty: if we should use tty; if None, will be auto-detected
199

200
  """
201
  args = ["ssh", "-oEscapeChar=none", "-oBatchMode=yes", "-lroot"]
202

    
203
  if tty is None:
204
    tty = sys.stdout.isatty()
205

    
206
  if tty:
207
    args.append("-t")
208

    
209
  if strict:
210
    tmp = "yes"
211
  else:
212
    tmp = "no"
213
  args.append("-oStrictHostKeyChecking=%s" % tmp)
214
  args.append("-oClearAllForwardings=yes")
215
  args.append("-oForwardAgent=yes")
216
  if opts:
217
    args.extend(opts)
218
  if node in _MULTIPLEXERS:
219
    spath = _MULTIPLEXERS[node][0]
220
    args.append("-oControlPath=%s" % spath)
221
    args.append("-oControlMaster=no")
222

    
223
  (vcluster_master, vcluster_basedir) = \
224
    qa_config.GetVclusterSettings()
225

    
226
  if vcluster_master:
227
    args.append(vcluster_master)
228
    args.append("%s/%s/cmd" % (vcluster_basedir, node))
229

    
230
    if cmd:
231
      # For virtual clusters the whole command must be wrapped using the "cmd"
232
      # script, as that script sets a number of environment variables. If the
233
      # command contains shell meta characters the whole command needs to be
234
      # quoted.
235
      args.append(utils.ShellQuote(cmd))
236
  else:
237
    args.append(node)
238

    
239
    if cmd:
240
      args.append(cmd)
241

    
242
  return args
243

    
244

    
245
def StartLocalCommand(cmd, _nolog_opts=False, log_cmd=True, **kwargs):
246
  """Starts a local command.
247

248
  """
249
  if log_cmd:
250
    if _nolog_opts:
251
      pcmd = [i for i in cmd if not i.startswith("-")]
252
    else:
253
      pcmd = cmd
254
    print "Command: %s" % utils.ShellQuoteArgs(pcmd)
255
  return subprocess.Popen(cmd, shell=False, **kwargs)
256

    
257

    
258
def StartSSH(node, cmd, strict=True, log_cmd=True):
259
  """Starts SSH.
260

261
  """
262
  return StartLocalCommand(GetSSHCommand(node, cmd, strict=strict),
263
                           _nolog_opts=True, log_cmd=log_cmd)
264

    
265

    
266
def StartMultiplexer(node):
267
  """Starts a multiplexer command.
268

269
  @param node: the node for which to open the multiplexer
270

271
  """
272
  if node in _MULTIPLEXERS:
273
    return
274

    
275
  # Note: yes, we only need mktemp, since we'll remove the file anyway
276
  sname = tempfile.mktemp(prefix="ganeti-qa-multiplexer.")
277
  utils.RemoveFile(sname)
278
  opts = ["-N", "-oControlPath=%s" % sname, "-oControlMaster=yes"]
279
  print "Created socket at %s" % sname
280
  child = StartLocalCommand(GetSSHCommand(node, None, opts=opts))
281
  _MULTIPLEXERS[node] = (sname, child)
282

    
283

    
284
def CloseMultiplexers():
285
  """Closes all current multiplexers and cleans up.
286

287
  """
288
  for node in _MULTIPLEXERS.keys():
289
    (sname, child) = _MULTIPLEXERS.pop(node)
290
    utils.KillProcess(child.pid, timeout=10, waitpid=True)
291
    utils.RemoveFile(sname)
292

    
293

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

297
  @type node: string
298
  @param node: node the command should run on
299
  @type cmd: string
300
  @param cmd: command to be executed in the node (cannot be empty or None)
301
  @type tty: bool or None
302
  @param tty: if we should use tty; if None, it will be auto-detected
303
  @type fail: bool
304
  @param fail: whether the command is expected to fail
305
  """
306
  assert cmd
307
  p = StartLocalCommand(GetSSHCommand(node, cmd, tty=tty),
308
                        stdout=subprocess.PIPE)
309
  rcode = p.wait()
310
  _AssertRetCode(rcode, fail, cmd, node)
311
  return p.stdout.read()
312

    
313

    
314
def GetObjectInfo(infocmd):
315
  """Get and parse information about a Ganeti object.
316

317
  @type infocmd: list of strings
318
  @param infocmd: command to be executed, e.g. ["gnt-cluster", "info"]
319
  @return: the information parsed, appropriately stored in dictionaries,
320
      lists...
321

322
  """
323
  master = qa_config.GetMasterNode()
324
  cmdline = utils.ShellQuoteArgs(infocmd)
325
  info_out = GetCommandOutput(master.primary, cmdline)
326
  return yaml.load(info_out)
327

    
328

    
329
def UploadFile(node, src):
330
  """Uploads a file to a node and returns the filename.
331

332
  Caller needs to remove the returned file on the node when it's not needed
333
  anymore.
334

335
  """
336
  # Make sure nobody else has access to it while preserving local permissions
337
  mode = os.stat(src).st_mode & 0700
338

    
339
  cmd = ('tmp=$(tempfile --mode %o --prefix gnt) && '
340
         '[[ -f "${tmp}" ]] && '
341
         'cat > "${tmp}" && '
342
         'echo "${tmp}"') % mode
343

    
344
  f = open(src, "r")
345
  try:
346
    p = subprocess.Popen(GetSSHCommand(node, cmd), shell=False, stdin=f,
347
                         stdout=subprocess.PIPE)
348
    AssertEqual(p.wait(), 0)
349

    
350
    # Return temporary filename
351
    return p.stdout.read().strip()
352
  finally:
353
    f.close()
354

    
355

    
356
def UploadData(node, data, mode=0600, filename=None):
357
  """Uploads data to a node and returns the filename.
358

359
  Caller needs to remove the returned file on the node when it's not needed
360
  anymore.
361

362
  """
363
  if filename:
364
    tmp = "tmp=%s" % utils.ShellQuote(filename)
365
  else:
366
    tmp = "tmp=$(tempfile --mode %o --prefix gnt)" % mode
367
  cmd = ("%s && "
368
         "[[ -f \"${tmp}\" ]] && "
369
         "cat > \"${tmp}\" && "
370
         "echo \"${tmp}\"") % tmp
371

    
372
  p = subprocess.Popen(GetSSHCommand(node, cmd), shell=False,
373
                       stdin=subprocess.PIPE, stdout=subprocess.PIPE)
374
  p.stdin.write(data)
375
  p.stdin.close()
376
  AssertEqual(p.wait(), 0)
377

    
378
  # Return temporary filename
379
  return p.stdout.read().strip()
380

    
381

    
382
def BackupFile(node, path):
383
  """Creates a backup of a file on the node and returns the filename.
384

385
  Caller needs to remove the returned file on the node when it's not needed
386
  anymore.
387

388
  """
389
  vpath = MakeNodePath(node, path)
390

    
391
  cmd = ("tmp=$(tempfile --prefix .gnt --directory=$(dirname %s)) && "
392
         "[[ -f \"$tmp\" ]] && "
393
         "cp %s $tmp && "
394
         "echo $tmp") % (utils.ShellQuote(vpath), utils.ShellQuote(vpath))
395

    
396
  # Return temporary filename
397
  result = GetCommandOutput(node, cmd).strip()
398

    
399
  print "Backup filename: %s" % result
400

    
401
  return result
402

    
403

    
404
def ResolveInstanceName(instance):
405
  """Gets the full name of an instance.
406

407
  @type instance: string
408
  @param instance: Instance name
409

410
  """
411
  info = GetObjectInfo(["gnt-instance", "info", instance])
412
  return info[0]["Instance name"]
413

    
414

    
415
def ResolveNodeName(node):
416
  """Gets the full name of a node.
417

418
  """
419
  info = GetObjectInfo(["gnt-node", "info", node.primary])
420
  return info[0]["Node name"]
421

    
422

    
423
def GetNodeInstances(node, secondaries=False):
424
  """Gets a list of instances on a node.
425

426
  """
427
  master = qa_config.GetMasterNode()
428
  node_name = ResolveNodeName(node)
429

    
430
  # Get list of all instances
431
  cmd = ["gnt-instance", "list", "--separator=:", "--no-headers",
432
         "--output=name,pnode,snodes"]
433
  output = GetCommandOutput(master.primary, utils.ShellQuoteArgs(cmd))
434

    
435
  instances = []
436
  for line in output.splitlines():
437
    (name, pnode, snodes) = line.split(":", 2)
438
    if ((not secondaries and pnode == node_name) or
439
        (secondaries and node_name in snodes.split(","))):
440
      instances.append(name)
441

    
442
  return instances
443

    
444

    
445
def _SelectQueryFields(rnd, fields):
446
  """Generates a list of fields for query tests.
447

448
  """
449
  # Create copy for shuffling
450
  fields = list(fields)
451
  rnd.shuffle(fields)
452

    
453
  # Check all fields
454
  yield fields
455
  yield sorted(fields)
456

    
457
  # Duplicate fields
458
  yield fields + fields
459

    
460
  # Check small groups of fields
461
  while fields:
462
    yield [fields.pop() for _ in range(rnd.randint(2, 10)) if fields]
463

    
464

    
465
def _List(listcmd, fields, names):
466
  """Runs a list command.
467

468
  """
469
  master = qa_config.GetMasterNode()
470

    
471
  cmd = [listcmd, "list", "--separator=|", "--no-headers",
472
         "--output", ",".join(fields)]
473

    
474
  if names:
475
    cmd.extend(names)
476

    
477
  return GetCommandOutput(master.primary,
478
                          utils.ShellQuoteArgs(cmd)).splitlines()
479

    
480

    
481
def GenericQueryTest(cmd, fields, namefield="name", test_unknown=True):
482
  """Runs a number of tests on query commands.
483

484
  @param cmd: Command name
485
  @param fields: List of field names
486

487
  """
488
  rnd = random.Random(hash(cmd))
489

    
490
  fields = list(fields)
491
  rnd.shuffle(fields)
492

    
493
  # Test a number of field combinations
494
  for testfields in _SelectQueryFields(rnd, fields):
495
    AssertRedirectedCommand([cmd, "list", "--output", ",".join(testfields)])
496

    
497
  if namefield is not None:
498
    namelist_fn = compat.partial(_List, cmd, [namefield])
499

    
500
    # When no names were requested, the list must be sorted
501
    names = namelist_fn(None)
502
    AssertEqual(names, utils.NiceSort(names))
503

    
504
    # When requesting specific names, the order must be kept
505
    revnames = list(reversed(names))
506
    AssertEqual(namelist_fn(revnames), revnames)
507

    
508
    randnames = list(names)
509
    rnd.shuffle(randnames)
510
    AssertEqual(namelist_fn(randnames), randnames)
511

    
512
  if test_unknown:
513
    # Listing unknown items must fail
514
    AssertCommand([cmd, "list", "this.name.certainly.does.not.exist"],
515
                  fail=True)
516

    
517
  # Check exit code for listing unknown field
518
  AssertEqual(AssertRedirectedCommand([cmd, "list",
519
                                       "--output=field/does/not/exist"],
520
                                      fail=True),
521
              constants.EXIT_UNKNOWN_FIELD)
522

    
523

    
524
def GenericQueryFieldsTest(cmd, fields):
525
  master = qa_config.GetMasterNode()
526

    
527
  # Listing fields
528
  AssertRedirectedCommand([cmd, "list-fields"])
529
  AssertRedirectedCommand([cmd, "list-fields"] + fields)
530

    
531
  # Check listed fields (all, must be sorted)
532
  realcmd = [cmd, "list-fields", "--separator=|", "--no-headers"]
533
  output = GetCommandOutput(master.primary,
534
                            utils.ShellQuoteArgs(realcmd)).splitlines()
535
  AssertEqual([line.split("|", 1)[0] for line in output],
536
              utils.NiceSort(fields))
537

    
538
  # Check exit code for listing unknown field
539
  AssertEqual(AssertCommand([cmd, "list-fields", "field/does/not/exist"],
540
                            fail=True),
541
              constants.EXIT_UNKNOWN_FIELD)
542

    
543

    
544
def AddToEtcHosts(hostnames):
545
  """Adds hostnames to /etc/hosts.
546

547
  @param hostnames: List of hostnames first used A records, all other CNAMEs
548

549
  """
550
  master = qa_config.GetMasterNode()
551
  tmp_hosts = UploadData(master.primary, "", mode=0644)
552

    
553
  data = []
554
  for localhost in ("::1", "127.0.0.1"):
555
    data.append("%s %s" % (localhost, " ".join(hostnames)))
556

    
557
  try:
558
    AssertCommand("{ cat %s && echo -e '%s'; } > %s && mv %s %s" %
559
                  (utils.ShellQuote(pathutils.ETC_HOSTS),
560
                   "\\n".join(data),
561
                   utils.ShellQuote(tmp_hosts),
562
                   utils.ShellQuote(tmp_hosts),
563
                   utils.ShellQuote(pathutils.ETC_HOSTS)))
564
  except Exception:
565
    AssertCommand(["rm", "-f", tmp_hosts])
566
    raise
567

    
568

    
569
def RemoveFromEtcHosts(hostnames):
570
  """Remove hostnames from /etc/hosts.
571

572
  @param hostnames: List of hostnames first used A records, all other CNAMEs
573

574
  """
575
  master = qa_config.GetMasterNode()
576
  tmp_hosts = UploadData(master.primary, "", mode=0644)
577
  quoted_tmp_hosts = utils.ShellQuote(tmp_hosts)
578

    
579
  sed_data = " ".join(hostnames)
580
  try:
581
    AssertCommand(("sed -e '/^\(::1\|127\.0\.0\.1\)\s\+%s/d' %s > %s"
582
                   " && mv %s %s") %
583
                   (sed_data, utils.ShellQuote(pathutils.ETC_HOSTS),
584
                    quoted_tmp_hosts, quoted_tmp_hosts,
585
                    utils.ShellQuote(pathutils.ETC_HOSTS)))
586
  except Exception:
587
    AssertCommand(["rm", "-f", tmp_hosts])
588
    raise
589

    
590

    
591
def RunInstanceCheck(instance, running):
592
  """Check if instance is running or not.
593

594
  """
595
  instance_name = _GetName(instance, operator.attrgetter("name"))
596

    
597
  script = qa_config.GetInstanceCheckScript()
598
  if not script:
599
    return
600

    
601
  master_node = qa_config.GetMasterNode()
602

    
603
  # Build command to connect to master node
604
  master_ssh = GetSSHCommand(master_node.primary, "--")
605

    
606
  if running:
607
    running_shellval = "1"
608
    running_text = ""
609
  else:
610
    running_shellval = ""
611
    running_text = "not "
612

    
613
  print FormatInfo("Checking if instance '%s' is %srunning" %
614
                   (instance_name, running_text))
615

    
616
  args = [script, instance_name]
617
  env = {
618
    "PATH": constants.HOOKS_PATH,
619
    "RUN_UUID": _RUN_UUID,
620
    "MASTER_SSH": utils.ShellQuoteArgs(master_ssh),
621
    "INSTANCE_NAME": instance_name,
622
    "INSTANCE_RUNNING": running_shellval,
623
    }
624

    
625
  result = os.spawnve(os.P_WAIT, script, args, env)
626
  if result != 0:
627
    raise qa_error.Error("Instance check failed with result %s" % result)
628

    
629

    
630
def _InstanceCheckInner(expected, instarg, args, result):
631
  """Helper function used by L{InstanceCheck}.
632

633
  """
634
  if instarg == FIRST_ARG:
635
    instance = args[0]
636
  elif instarg == RETURN_VALUE:
637
    instance = result
638
  else:
639
    raise Exception("Invalid value '%s' for instance argument" % instarg)
640

    
641
  if expected in (INST_DOWN, INST_UP):
642
    RunInstanceCheck(instance, (expected == INST_UP))
643
  elif expected is not None:
644
    raise Exception("Invalid value '%s'" % expected)
645

    
646

    
647
def InstanceCheck(before, after, instarg):
648
  """Decorator to check instance status before and after test.
649

650
  @param before: L{INST_DOWN} if instance must be stopped before test,
651
    L{INST_UP} if instance must be running before test, L{None} to not check.
652
  @param after: L{INST_DOWN} if instance must be stopped after test,
653
    L{INST_UP} if instance must be running after test, L{None} to not check.
654
  @param instarg: L{FIRST_ARG} to use first argument to test as instance (a
655
    dictionary), L{RETURN_VALUE} to use return value (disallows pre-checks)
656

657
  """
658
  def decorator(fn):
659
    @functools.wraps(fn)
660
    def wrapper(*args, **kwargs):
661
      _InstanceCheckInner(before, instarg, args, NotImplemented)
662

    
663
      result = fn(*args, **kwargs)
664

    
665
      _InstanceCheckInner(after, instarg, args, result)
666

    
667
      return result
668
    return wrapper
669
  return decorator
670

    
671

    
672
def GetNonexistentGroups(count):
673
  """Gets group names which shouldn't exist on the cluster.
674

675
  @param count: Number of groups to get
676
  @rtype: integer
677

678
  """
679
  return GetNonexistentEntityNames(count, "groups", "group")
680

    
681

    
682
def GetNonexistentEntityNames(count, name_config, name_prefix):
683
  """Gets entity names which shouldn't exist on the cluster.
684

685
  The actualy names can refer to arbitrary entities (for example
686
  groups, networks).
687

688
  @param count: Number of names to get
689
  @rtype: integer
690
  @param name_config: name of the leaf in the config containing
691
    this entity's configuration, including a 'inexistent-'
692
    element
693
  @rtype: string
694
  @param name_prefix: prefix of the entity's names, used to compose
695
    the default values; for example for groups, the prefix is
696
    'group' and the generated names are then group1, group2, ...
697
  @rtype: string
698

699
  """
700
  entities = qa_config.get(name_config, {})
701

    
702
  default = [name_prefix + str(i) for i in range(count)]
703
  assert count <= len(default)
704

    
705
  name_config_inexistent = "inexistent-" + name_config
706
  candidates = entities.get(name_config_inexistent, default)[:count]
707

    
708
  if len(candidates) < count:
709
    raise Exception("At least %s non-existent %s are needed" %
710
                    (count, name_config))
711

    
712
  return candidates
713

    
714

    
715
def MakeNodePath(node, path):
716
  """Builds an absolute path for a virtual node.
717

718
  @type node: string or L{qa_config._QaNode}
719
  @param node: Node
720
  @type path: string
721
  @param path: Path without node-specific prefix
722

723
  """
724
  (_, basedir) = qa_config.GetVclusterSettings()
725

    
726
  if isinstance(node, basestring):
727
    name = node
728
  else:
729
    name = node.primary
730

    
731
  if basedir:
732
    assert path.startswith("/")
733
    return "%s%s" % (vcluster.MakeNodeRoot(basedir, name), path)
734
  else:
735
    return path
736

    
737

    
738
def _GetParameterOptions(specs):
739
  """Helper to build policy options."""
740
  values = ["%s=%s" % (par, val)
741
            for (par, val) in specs.items()]
742
  return ",".join(values)
743

    
744

    
745
def TestSetISpecs(new_specs=None, diff_specs=None, get_policy_fn=None,
746
                  build_cmd_fn=None, fail=False, old_values=None):
747
  """Change instance specs for an object.
748

749
  At most one of new_specs or diff_specs can be specified.
750

751
  @type new_specs: dict
752
  @param new_specs: new complete specs, in the same format returned by
753
      L{ParseIPolicy}.
754
  @type diff_specs: dict
755
  @param diff_specs: partial specs, it can be an incomplete specifications, but
756
      if min/max specs are specified, their number must match the number of the
757
      existing specs
758
  @type get_policy_fn: function
759
  @param get_policy_fn: function that returns the current policy as in
760
      L{ParseIPolicy}
761
  @type build_cmd_fn: function
762
  @param build_cmd_fn: function that return the full command line from the
763
      options alone
764
  @type fail: bool
765
  @param fail: if the change is expected to fail
766
  @type old_values: tuple
767
  @param old_values: (old_policy, old_specs), as returned by
768
     L{ParseIPolicy}
769
  @return: same as L{ParseIPolicy}
770

771
  """
772
  assert get_policy_fn is not None
773
  assert build_cmd_fn is not None
774
  assert new_specs is None or diff_specs is None
775

    
776
  if old_values:
777
    (old_policy, old_specs) = old_values
778
  else:
779
    (old_policy, old_specs) = get_policy_fn()
780

    
781
  if diff_specs:
782
    new_specs = copy.deepcopy(old_specs)
783
    if constants.ISPECS_MINMAX in diff_specs:
784
      AssertEqual(len(new_specs[constants.ISPECS_MINMAX]),
785
                  len(diff_specs[constants.ISPECS_MINMAX]))
786
      for (new_minmax, diff_minmax) in zip(new_specs[constants.ISPECS_MINMAX],
787
                                           diff_specs[constants.ISPECS_MINMAX]):
788
        for (key, parvals) in diff_minmax.items():
789
          for (par, val) in parvals.items():
790
            new_minmax[key][par] = val
791
    for (par, val) in diff_specs.get(constants.ISPECS_STD, {}).items():
792
      new_specs[constants.ISPECS_STD][par] = val
793

    
794
  if new_specs:
795
    cmd = []
796
    if (diff_specs is None or constants.ISPECS_MINMAX in diff_specs):
797
      minmax_opt_items = []
798
      for minmax in new_specs[constants.ISPECS_MINMAX]:
799
        minmax_opts = []
800
        for key in ["min", "max"]:
801
          keyopt = _GetParameterOptions(minmax[key])
802
          minmax_opts.append("%s:%s" % (key, keyopt))
803
        minmax_opt_items.append("/".join(minmax_opts))
804
      cmd.extend([
805
        "--ipolicy-bounds-specs",
806
        "//".join(minmax_opt_items)
807
        ])
808
    if diff_specs is None:
809
      std_source = new_specs
810
    else:
811
      std_source = diff_specs
812
    std_opt = _GetParameterOptions(std_source.get("std", {}))
813
    if std_opt:
814
      cmd.extend(["--ipolicy-std-specs", std_opt])
815
    AssertCommand(build_cmd_fn(cmd), fail=fail)
816

    
817
    # Check the new state
818
    (eff_policy, eff_specs) = get_policy_fn()
819
    AssertEqual(eff_policy, old_policy)
820
    if fail:
821
      AssertEqual(eff_specs, old_specs)
822
    else:
823
      AssertEqual(eff_specs, new_specs)
824

    
825
  else:
826
    (eff_policy, eff_specs) = (old_policy, old_specs)
827

    
828
  return (eff_policy, eff_specs)
829

    
830

    
831
def ParseIPolicy(policy):
832
  """Parse and split instance an instance policy.
833

834
  @type policy: dict
835
  @param policy: policy, as returned by L{GetObjectInfo}
836
  @rtype: tuple
837
  @return: (policy, specs), where:
838
      - policy is a dictionary of the policy values, instance specs excluded
839
      - specs is a dictionary containing only the specs, using the internal
840
        format (see L{constants.IPOLICY_DEFAULTS} for an example)
841

842
  """
843
  ret_specs = {}
844
  ret_policy = {}
845
  for (key, val) in policy.items():
846
    if key == "bounds specs":
847
      ret_specs[constants.ISPECS_MINMAX] = []
848
      for minmax in val:
849
        ret_minmax = {}
850
        for key in minmax:
851
          keyparts = key.split("/", 1)
852
          assert len(keyparts) > 1
853
          ret_minmax[keyparts[0]] = minmax[key]
854
        ret_specs[constants.ISPECS_MINMAX].append(ret_minmax)
855
    elif key == constants.ISPECS_STD:
856
      ret_specs[key] = val
857
    else:
858
      ret_policy[key] = val
859
  return (ret_policy, ret_specs)