Update NEWS file
[ganeti-local] / qa / qa_utils.py
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
52 _INFO_SEQ = None
53 _WARNING_SEQ = None
54 _ERROR_SEQ = None
55 _RESET_SEQ = None
56
57 _MULTIPLEXERS = {}
58
59 #: Unique ID per QA run
60 _RUN_UUID = utils.NewUUID()
61
62 #: Path to the QA query output log file
63 _QA_OUTPUT = pathutils.GetLogFilename("qa-output")
64
65
66 (INST_DOWN,
67  INST_UP) = range(500, 502)
68
69 (FIRST_ARG,
70  RETURN_VALUE) = range(1000, 1002)
71
72
73 def _SetupColours():
74   """Initializes the colour constants.
75
76   """
77   # pylint: disable=W0603
78   # due to global usage
79   global _INFO_SEQ, _WARNING_SEQ, _ERROR_SEQ, _RESET_SEQ
80
81   # Don't use colours if stdout isn't a terminal
82   if not sys.stdout.isatty():
83     return
84
85   try:
86     import curses
87   except ImportError:
88     # Don't use colours if curses module can't be imported
89     return
90
91   curses.setupterm()
92
93   _RESET_SEQ = curses.tigetstr("op")
94
95   setaf = curses.tigetstr("setaf")
96   _INFO_SEQ = curses.tparm(setaf, curses.COLOR_GREEN)
97   _WARNING_SEQ = curses.tparm(setaf, curses.COLOR_YELLOW)
98   _ERROR_SEQ = curses.tparm(setaf, curses.COLOR_RED)
99
100
101 _SetupColours()
102
103
104 def AssertIn(item, sequence):
105   """Raises an error when item is not in sequence.
106
107   """
108   if item not in sequence:
109     raise qa_error.Error("%r not in %r" % (item, sequence))
110
111
112 def AssertNotIn(item, sequence):
113   """Raises an error when item is in sequence.
114
115   """
116   if item in sequence:
117     raise qa_error.Error("%r in %r" % (item, sequence))
118
119
120 def AssertEqual(first, second):
121   """Raises an error when values aren't equal.
122
123   """
124   if not first == second:
125     raise qa_error.Error("%r == %r" % (first, second))
126
127
128 def AssertMatch(string, pattern):
129   """Raises an error when string doesn't match regexp pattern.
130
131   """
132   if not re.match(pattern, string):
133     raise qa_error.Error("%r doesn't match /%r/" % (string, pattern))
134
135
136 def _GetName(entity, fn):
137   """Tries to get name of an entity.
138
139   @type entity: string or dict
140   @param fn: Function retrieving name from entity
141
142   """
143   if isinstance(entity, basestring):
144     result = entity
145   else:
146     result = fn(entity)
147
148   if not ht.TNonEmptyString(result):
149     raise Exception("Invalid name '%s'" % result)
150
151   return result
152
153
154 def _AssertRetCode(rcode, fail, cmdstr, nodename):
155   """Check the return value from a command and possibly raise an exception.
156
157   """
158   if fail and rcode == 0:
159     raise qa_error.Error("Command '%s' on node %s was expected to fail but"
160                          " didn't" % (cmdstr, nodename))
161   elif not fail and rcode != 0:
162     raise qa_error.Error("Command '%s' on node %s failed, exit code %s" %
163                          (cmdstr, nodename, rcode))
164
165
166 def AssertCommand(cmd, fail=False, node=None, log_cmd=True):
167   """Checks that a remote command succeeds.
168
169   @param cmd: either a string (the command to execute) or a list (to
170       be converted using L{utils.ShellQuoteArgs} into a string)
171   @type fail: boolean
172   @param fail: if the command is expected to fail instead of succeeding
173   @param node: if passed, it should be the node on which the command
174       should be executed, instead of the master node (can be either a
175       dict or a string)
176   @param log_cmd: if False, the command won't be logged (simply passed to
177       StartSSH)
178   @return: the return code of the command
179   @raise qa_error.Error: if the command fails when it shouldn't or vice versa
180
181   """
182   if node is None:
183     node = qa_config.GetMasterNode()
184
185   nodename = _GetName(node, operator.attrgetter("primary"))
186
187   if isinstance(cmd, basestring):
188     cmdstr = cmd
189   else:
190     cmdstr = utils.ShellQuoteArgs(cmd)
191
192   rcode = StartSSH(nodename, cmdstr, log_cmd=log_cmd).wait()
193   _AssertRetCode(rcode, fail, cmdstr, nodename)
194
195   return rcode
196
197
198 def AssertRedirectedCommand(cmd, fail=False, node=None, log_cmd=True):
199   """Executes a command with redirected output.
200
201   The log will go to the qa-output log file in the ganeti log
202   directory on the node where the command is executed. The fail and
203   node parameters are passed unchanged to AssertCommand.
204
205   @param cmd: the command to be executed, as a list; a string is not
206       supported
207
208   """
209   if not isinstance(cmd, list):
210     raise qa_error.Error("Non-list passed to AssertRedirectedCommand")
211   ofile = utils.ShellQuote(_QA_OUTPUT)
212   cmdstr = utils.ShellQuoteArgs(cmd)
213   AssertCommand("echo ---- $(date) %s ---- >> %s" % (cmdstr, ofile),
214                 fail=False, node=node, log_cmd=False)
215   return AssertCommand(cmdstr + " >> %s" % ofile,
216                        fail=fail, node=node, log_cmd=log_cmd)
217
218
219 def GetSSHCommand(node, cmd, strict=True, opts=None, tty=None):
220   """Builds SSH command to be executed.
221
222   @type node: string
223   @param node: node the command should run on
224   @type cmd: string
225   @param cmd: command to be executed in the node; if None or empty
226       string, no command will be executed
227   @type strict: boolean
228   @param strict: whether to enable strict host key checking
229   @type opts: list
230   @param opts: list of additional options
231   @type tty: boolean or None
232   @param tty: if we should use tty; if None, will be auto-detected
233
234   """
235   args = ["ssh", "-oEscapeChar=none", "-oBatchMode=yes", "-lroot"]
236
237   if tty is None:
238     tty = sys.stdout.isatty()
239
240   if tty:
241     args.append("-t")
242
243   if strict:
244     tmp = "yes"
245   else:
246     tmp = "no"
247   args.append("-oStrictHostKeyChecking=%s" % tmp)
248   args.append("-oClearAllForwardings=yes")
249   args.append("-oForwardAgent=yes")
250   if opts:
251     args.extend(opts)
252   if node in _MULTIPLEXERS:
253     spath = _MULTIPLEXERS[node][0]
254     args.append("-oControlPath=%s" % spath)
255     args.append("-oControlMaster=no")
256
257   (vcluster_master, vcluster_basedir) = \
258     qa_config.GetVclusterSettings()
259
260   if vcluster_master:
261     args.append(vcluster_master)
262     args.append("%s/%s/cmd" % (vcluster_basedir, node))
263
264     if cmd:
265       # For virtual clusters the whole command must be wrapped using the "cmd"
266       # script, as that script sets a number of environment variables. If the
267       # command contains shell meta characters the whole command needs to be
268       # quoted.
269       args.append(utils.ShellQuote(cmd))
270   else:
271     args.append(node)
272
273     if cmd:
274       args.append(cmd)
275
276   return args
277
278
279 def StartLocalCommand(cmd, _nolog_opts=False, log_cmd=True, **kwargs):
280   """Starts a local command.
281
282   """
283   if log_cmd:
284     if _nolog_opts:
285       pcmd = [i for i in cmd if not i.startswith("-")]
286     else:
287       pcmd = cmd
288     print "Command: %s" % utils.ShellQuoteArgs(pcmd)
289   return subprocess.Popen(cmd, shell=False, **kwargs)
290
291
292 def StartSSH(node, cmd, strict=True, log_cmd=True):
293   """Starts SSH.
294
295   """
296   return StartLocalCommand(GetSSHCommand(node, cmd, strict=strict),
297                            _nolog_opts=True, log_cmd=log_cmd)
298
299
300 def StartMultiplexer(node):
301   """Starts a multiplexer command.
302
303   @param node: the node for which to open the multiplexer
304
305   """
306   if node in _MULTIPLEXERS:
307     return
308
309   # Note: yes, we only need mktemp, since we'll remove the file anyway
310   sname = tempfile.mktemp(prefix="ganeti-qa-multiplexer.")
311   utils.RemoveFile(sname)
312   opts = ["-N", "-oControlPath=%s" % sname, "-oControlMaster=yes"]
313   print "Created socket at %s" % sname
314   child = StartLocalCommand(GetSSHCommand(node, None, opts=opts))
315   _MULTIPLEXERS[node] = (sname, child)
316
317
318 def CloseMultiplexers():
319   """Closes all current multiplexers and cleans up.
320
321   """
322   for node in _MULTIPLEXERS.keys():
323     (sname, child) = _MULTIPLEXERS.pop(node)
324     utils.KillProcess(child.pid, timeout=10, waitpid=True)
325     utils.RemoveFile(sname)
326
327
328 def GetCommandOutput(node, cmd, tty=None, fail=False):
329   """Returns the output of a command executed on the given node.
330
331   @type node: string
332   @param node: node the command should run on
333   @type cmd: string
334   @param cmd: command to be executed in the node (cannot be empty or None)
335   @type tty: bool or None
336   @param tty: if we should use tty; if None, it will be auto-detected
337   @type fail: bool
338   @param fail: whether the command is expected to fail
339   """
340   assert cmd
341   p = StartLocalCommand(GetSSHCommand(node, cmd, tty=tty),
342                         stdout=subprocess.PIPE)
343   rcode = p.wait()
344   _AssertRetCode(rcode, fail, cmd, node)
345   return p.stdout.read()
346
347
348 def GetObjectInfo(infocmd):
349   """Get and parse information about a Ganeti object.
350
351   @type infocmd: list of strings
352   @param infocmd: command to be executed, e.g. ["gnt-cluster", "info"]
353   @return: the information parsed, appropriately stored in dictionaries,
354       lists...
355
356   """
357   master = qa_config.GetMasterNode()
358   cmdline = utils.ShellQuoteArgs(infocmd)
359   info_out = GetCommandOutput(master.primary, cmdline)
360   return yaml.load(info_out)
361
362
363 def UploadFile(node, src):
364   """Uploads a file to a node and returns the filename.
365
366   Caller needs to remove the returned file on the node when it's not needed
367   anymore.
368
369   """
370   # Make sure nobody else has access to it while preserving local permissions
371   mode = os.stat(src).st_mode & 0700
372
373   cmd = ('tmp=$(mktemp --tmpdir gnt.XXXXXX) && '
374          'chmod %o "${tmp}" && '
375          '[[ -f "${tmp}" ]] && '
376          'cat > "${tmp}" && '
377          'echo "${tmp}"') % mode
378
379   f = open(src, "r")
380   try:
381     p = subprocess.Popen(GetSSHCommand(node, cmd), shell=False, stdin=f,
382                          stdout=subprocess.PIPE)
383     AssertEqual(p.wait(), 0)
384
385     # Return temporary filename
386     return p.stdout.read().strip()
387   finally:
388     f.close()
389
390
391 def UploadData(node, data, mode=0600, filename=None):
392   """Uploads data to a node and returns the filename.
393
394   Caller needs to remove the returned file on the node when it's not needed
395   anymore.
396
397   """
398   if filename:
399     tmp = "tmp=%s" % utils.ShellQuote(filename)
400   else:
401     tmp = ('tmp=$(mktemp --tmpdir gnt.XXXXXX) && '
402            'chmod %o "${tmp}"') % mode
403   cmd = ("%s && "
404          "[[ -f \"${tmp}\" ]] && "
405          "cat > \"${tmp}\" && "
406          "echo \"${tmp}\"") % tmp
407
408   p = subprocess.Popen(GetSSHCommand(node, cmd), shell=False,
409                        stdin=subprocess.PIPE, stdout=subprocess.PIPE)
410   p.stdin.write(data)
411   p.stdin.close()
412   AssertEqual(p.wait(), 0)
413
414   # Return temporary filename
415   return p.stdout.read().strip()
416
417
418 def BackupFile(node, path):
419   """Creates a backup of a file on the node and returns the filename.
420
421   Caller needs to remove the returned file on the node when it's not needed
422   anymore.
423
424   """
425   vpath = MakeNodePath(node, path)
426
427   cmd = ("tmp=$(mktemp .gnt.XXXXXX --tmpdir=$(dirname %s)) && "
428          "[[ -f \"$tmp\" ]] && "
429          "cp %s $tmp && "
430          "echo $tmp") % (utils.ShellQuote(vpath), utils.ShellQuote(vpath))
431
432   # Return temporary filename
433   result = GetCommandOutput(node, cmd).strip()
434
435   print "Backup filename: %s" % result
436
437   return result
438
439
440 def ResolveInstanceName(instance):
441   """Gets the full name of an instance.
442
443   @type instance: string
444   @param instance: Instance name
445
446   """
447   info = GetObjectInfo(["gnt-instance", "info", instance])
448   return info[0]["Instance name"]
449
450
451 def ResolveNodeName(node):
452   """Gets the full name of a node.
453
454   """
455   info = GetObjectInfo(["gnt-node", "info", node.primary])
456   return info[0]["Node name"]
457
458
459 def GetNodeInstances(node, secondaries=False):
460   """Gets a list of instances on a node.
461
462   """
463   master = qa_config.GetMasterNode()
464   node_name = ResolveNodeName(node)
465
466   # Get list of all instances
467   cmd = ["gnt-instance", "list", "--separator=:", "--no-headers",
468          "--output=name,pnode,snodes"]
469   output = GetCommandOutput(master.primary, utils.ShellQuoteArgs(cmd))
470
471   instances = []
472   for line in output.splitlines():
473     (name, pnode, snodes) = line.split(":", 2)
474     if ((not secondaries and pnode == node_name) or
475         (secondaries and node_name in snodes.split(","))):
476       instances.append(name)
477
478   return instances
479
480
481 def _SelectQueryFields(rnd, fields):
482   """Generates a list of fields for query tests.
483
484   """
485   # Create copy for shuffling
486   fields = list(fields)
487   rnd.shuffle(fields)
488
489   # Check all fields
490   yield fields
491   yield sorted(fields)
492
493   # Duplicate fields
494   yield fields + fields
495
496   # Check small groups of fields
497   while fields:
498     yield [fields.pop() for _ in range(rnd.randint(2, 10)) if fields]
499
500
501 def _List(listcmd, fields, names):
502   """Runs a list command.
503
504   """
505   master = qa_config.GetMasterNode()
506
507   cmd = [listcmd, "list", "--separator=|", "--no-headers",
508          "--output", ",".join(fields)]
509
510   if names:
511     cmd.extend(names)
512
513   return GetCommandOutput(master.primary,
514                           utils.ShellQuoteArgs(cmd)).splitlines()
515
516
517 def GenericQueryTest(cmd, fields, namefield="name", test_unknown=True):
518   """Runs a number of tests on query commands.
519
520   @param cmd: Command name
521   @param fields: List of field names
522
523   """
524   rnd = random.Random(hash(cmd))
525
526   fields = list(fields)
527   rnd.shuffle(fields)
528
529   # Test a number of field combinations
530   for testfields in _SelectQueryFields(rnd, fields):
531     AssertRedirectedCommand([cmd, "list", "--output", ",".join(testfields)])
532
533   if namefield is not None:
534     namelist_fn = compat.partial(_List, cmd, [namefield])
535
536     # When no names were requested, the list must be sorted
537     names = namelist_fn(None)
538     AssertEqual(names, utils.NiceSort(names))
539
540     # When requesting specific names, the order must be kept
541     revnames = list(reversed(names))
542     AssertEqual(namelist_fn(revnames), revnames)
543
544     randnames = list(names)
545     rnd.shuffle(randnames)
546     AssertEqual(namelist_fn(randnames), randnames)
547
548   if test_unknown:
549     # Listing unknown items must fail
550     AssertCommand([cmd, "list", "this.name.certainly.does.not.exist"],
551                   fail=True)
552
553   # Check exit code for listing unknown field
554   AssertEqual(AssertRedirectedCommand([cmd, "list",
555                                        "--output=field/does/not/exist"],
556                                       fail=True),
557               constants.EXIT_UNKNOWN_FIELD)
558
559
560 def GenericQueryFieldsTest(cmd, fields):
561   master = qa_config.GetMasterNode()
562
563   # Listing fields
564   AssertRedirectedCommand([cmd, "list-fields"])
565   AssertRedirectedCommand([cmd, "list-fields"] + fields)
566
567   # Check listed fields (all, must be sorted)
568   realcmd = [cmd, "list-fields", "--separator=|", "--no-headers"]
569   output = GetCommandOutput(master.primary,
570                             utils.ShellQuoteArgs(realcmd)).splitlines()
571   AssertEqual([line.split("|", 1)[0] for line in output],
572               utils.NiceSort(fields))
573
574   # Check exit code for listing unknown field
575   AssertEqual(AssertCommand([cmd, "list-fields", "field/does/not/exist"],
576                             fail=True),
577               constants.EXIT_UNKNOWN_FIELD)
578
579
580 def _FormatWithColor(text, seq):
581   if not seq:
582     return text
583   return "%s%s%s" % (seq, text, _RESET_SEQ)
584
585
586 FormatWarning = lambda text: _FormatWithColor(text, _WARNING_SEQ)
587 FormatError = lambda text: _FormatWithColor(text, _ERROR_SEQ)
588 FormatInfo = lambda text: _FormatWithColor(text, _INFO_SEQ)
589
590
591 def AddToEtcHosts(hostnames):
592   """Adds hostnames to /etc/hosts.
593
594   @param hostnames: List of hostnames first used A records, all other CNAMEs
595
596   """
597   master = qa_config.GetMasterNode()
598   tmp_hosts = UploadData(master.primary, "", mode=0644)
599
600   data = []
601   for localhost in ("::1", "127.0.0.1"):
602     data.append("%s %s" % (localhost, " ".join(hostnames)))
603
604   try:
605     AssertCommand("{ cat %s && echo -e '%s'; } > %s && mv %s %s" %
606                   (utils.ShellQuote(pathutils.ETC_HOSTS),
607                    "\\n".join(data),
608                    utils.ShellQuote(tmp_hosts),
609                    utils.ShellQuote(tmp_hosts),
610                    utils.ShellQuote(pathutils.ETC_HOSTS)))
611   except Exception:
612     AssertCommand(["rm", "-f", tmp_hosts])
613     raise
614
615
616 def RemoveFromEtcHosts(hostnames):
617   """Remove hostnames from /etc/hosts.
618
619   @param hostnames: List of hostnames first used A records, all other CNAMEs
620
621   """
622   master = qa_config.GetMasterNode()
623   tmp_hosts = UploadData(master.primary, "", mode=0644)
624   quoted_tmp_hosts = utils.ShellQuote(tmp_hosts)
625
626   sed_data = " ".join(hostnames)
627   try:
628     AssertCommand((r"sed -e '/^\(::1\|127\.0\.0\.1\)\s\+%s/d' %s > %s"
629                    r" && mv %s %s") %
630                    (sed_data, utils.ShellQuote(pathutils.ETC_HOSTS),
631                     quoted_tmp_hosts, quoted_tmp_hosts,
632                     utils.ShellQuote(pathutils.ETC_HOSTS)))
633   except Exception:
634     AssertCommand(["rm", "-f", tmp_hosts])
635     raise
636
637
638 def RunInstanceCheck(instance, running):
639   """Check if instance is running or not.
640
641   """
642   instance_name = _GetName(instance, operator.attrgetter("name"))
643
644   script = qa_config.GetInstanceCheckScript()
645   if not script:
646     return
647
648   master_node = qa_config.GetMasterNode()
649
650   # Build command to connect to master node
651   master_ssh = GetSSHCommand(master_node.primary, "--")
652
653   if running:
654     running_shellval = "1"
655     running_text = ""
656   else:
657     running_shellval = ""
658     running_text = "not "
659
660   print FormatInfo("Checking if instance '%s' is %srunning" %
661                    (instance_name, running_text))
662
663   args = [script, instance_name]
664   env = {
665     "PATH": constants.HOOKS_PATH,
666     "RUN_UUID": _RUN_UUID,
667     "MASTER_SSH": utils.ShellQuoteArgs(master_ssh),
668     "INSTANCE_NAME": instance_name,
669     "INSTANCE_RUNNING": running_shellval,
670     }
671
672   result = os.spawnve(os.P_WAIT, script, args, env)
673   if result != 0:
674     raise qa_error.Error("Instance check failed with result %s" % result)
675
676
677 def _InstanceCheckInner(expected, instarg, args, result):
678   """Helper function used by L{InstanceCheck}.
679
680   """
681   if instarg == FIRST_ARG:
682     instance = args[0]
683   elif instarg == RETURN_VALUE:
684     instance = result
685   else:
686     raise Exception("Invalid value '%s' for instance argument" % instarg)
687
688   if expected in (INST_DOWN, INST_UP):
689     RunInstanceCheck(instance, (expected == INST_UP))
690   elif expected is not None:
691     raise Exception("Invalid value '%s'" % expected)
692
693
694 def InstanceCheck(before, after, instarg):
695   """Decorator to check instance status before and after test.
696
697   @param before: L{INST_DOWN} if instance must be stopped before test,
698     L{INST_UP} if instance must be running before test, L{None} to not check.
699   @param after: L{INST_DOWN} if instance must be stopped after test,
700     L{INST_UP} if instance must be running after test, L{None} to not check.
701   @param instarg: L{FIRST_ARG} to use first argument to test as instance (a
702     dictionary), L{RETURN_VALUE} to use return value (disallows pre-checks)
703
704   """
705   def decorator(fn):
706     @functools.wraps(fn)
707     def wrapper(*args, **kwargs):
708       _InstanceCheckInner(before, instarg, args, NotImplemented)
709
710       result = fn(*args, **kwargs)
711
712       _InstanceCheckInner(after, instarg, args, result)
713
714       return result
715     return wrapper
716   return decorator
717
718
719 def GetNonexistentGroups(count):
720   """Gets group names which shouldn't exist on the cluster.
721
722   @param count: Number of groups to get
723   @rtype: integer
724
725   """
726   return GetNonexistentEntityNames(count, "groups", "group")
727
728
729 def GetNonexistentEntityNames(count, name_config, name_prefix):
730   """Gets entity names which shouldn't exist on the cluster.
731
732   The actualy names can refer to arbitrary entities (for example
733   groups, networks).
734
735   @param count: Number of names to get
736   @rtype: integer
737   @param name_config: name of the leaf in the config containing
738     this entity's configuration, including a 'inexistent-'
739     element
740   @rtype: string
741   @param name_prefix: prefix of the entity's names, used to compose
742     the default values; for example for groups, the prefix is
743     'group' and the generated names are then group1, group2, ...
744   @rtype: string
745
746   """
747   entities = qa_config.get(name_config, {})
748
749   default = [name_prefix + str(i) for i in range(count)]
750   assert count <= len(default)
751
752   name_config_inexistent = "inexistent-" + name_config
753   candidates = entities.get(name_config_inexistent, default)[:count]
754
755   if len(candidates) < count:
756     raise Exception("At least %s non-existent %s are needed" %
757                     (count, name_config))
758
759   return candidates
760
761
762 def MakeNodePath(node, path):
763   """Builds an absolute path for a virtual node.
764
765   @type node: string or L{qa_config._QaNode}
766   @param node: Node
767   @type path: string
768   @param path: Path without node-specific prefix
769
770   """
771   (_, basedir) = qa_config.GetVclusterSettings()
772
773   if isinstance(node, basestring):
774     name = node
775   else:
776     name = node.primary
777
778   if basedir:
779     assert path.startswith("/")
780     return "%s%s" % (vcluster.MakeNodeRoot(basedir, name), path)
781   else:
782     return path
783
784
785 def _GetParameterOptions(specs):
786   """Helper to build policy options."""
787   values = ["%s=%s" % (par, val)
788             for (par, val) in specs.items()]
789   return ",".join(values)
790
791
792 def TestSetISpecs(new_specs=None, diff_specs=None, get_policy_fn=None,
793                   build_cmd_fn=None, fail=False, old_values=None):
794   """Change instance specs for an object.
795
796   At most one of new_specs or diff_specs can be specified.
797
798   @type new_specs: dict
799   @param new_specs: new complete specs, in the same format returned by
800       L{ParseIPolicy}.
801   @type diff_specs: dict
802   @param diff_specs: partial specs, it can be an incomplete specifications, but
803       if min/max specs are specified, their number must match the number of the
804       existing specs
805   @type get_policy_fn: function
806   @param get_policy_fn: function that returns the current policy as in
807       L{ParseIPolicy}
808   @type build_cmd_fn: function
809   @param build_cmd_fn: function that return the full command line from the
810       options alone
811   @type fail: bool
812   @param fail: if the change is expected to fail
813   @type old_values: tuple
814   @param old_values: (old_policy, old_specs), as returned by
815      L{ParseIPolicy}
816   @return: same as L{ParseIPolicy}
817
818   """
819   assert get_policy_fn is not None
820   assert build_cmd_fn is not None
821   assert new_specs is None or diff_specs is None
822
823   if old_values:
824     (old_policy, old_specs) = old_values
825   else:
826     (old_policy, old_specs) = get_policy_fn()
827
828   if diff_specs:
829     new_specs = copy.deepcopy(old_specs)
830     if constants.ISPECS_MINMAX in diff_specs:
831       AssertEqual(len(new_specs[constants.ISPECS_MINMAX]),
832                   len(diff_specs[constants.ISPECS_MINMAX]))
833       for (new_minmax, diff_minmax) in zip(new_specs[constants.ISPECS_MINMAX],
834                                            diff_specs[constants.ISPECS_MINMAX]):
835         for (key, parvals) in diff_minmax.items():
836           for (par, val) in parvals.items():
837             new_minmax[key][par] = val
838     for (par, val) in diff_specs.get(constants.ISPECS_STD, {}).items():
839       new_specs[constants.ISPECS_STD][par] = val
840
841   if new_specs:
842     cmd = []
843     if (diff_specs is None or constants.ISPECS_MINMAX in diff_specs):
844       minmax_opt_items = []
845       for minmax in new_specs[constants.ISPECS_MINMAX]:
846         minmax_opts = []
847         for key in ["min", "max"]:
848           keyopt = _GetParameterOptions(minmax[key])
849           minmax_opts.append("%s:%s" % (key, keyopt))
850         minmax_opt_items.append("/".join(minmax_opts))
851       cmd.extend([
852         "--ipolicy-bounds-specs",
853         "//".join(minmax_opt_items)
854         ])
855     if diff_specs is None:
856       std_source = new_specs
857     else:
858       std_source = diff_specs
859     std_opt = _GetParameterOptions(std_source.get("std", {}))
860     if std_opt:
861       cmd.extend(["--ipolicy-std-specs", std_opt])
862     AssertCommand(build_cmd_fn(cmd), fail=fail)
863
864     # Check the new state
865     (eff_policy, eff_specs) = get_policy_fn()
866     AssertEqual(eff_policy, old_policy)
867     if fail:
868       AssertEqual(eff_specs, old_specs)
869     else:
870       AssertEqual(eff_specs, new_specs)
871
872   else:
873     (eff_policy, eff_specs) = (old_policy, old_specs)
874
875   return (eff_policy, eff_specs)
876
877
878 def ParseIPolicy(policy):
879   """Parse and split instance an instance policy.
880
881   @type policy: dict
882   @param policy: policy, as returned by L{GetObjectInfo}
883   @rtype: tuple
884   @return: (policy, specs), where:
885       - policy is a dictionary of the policy values, instance specs excluded
886       - specs is a dictionary containing only the specs, using the internal
887         format (see L{constants.IPOLICY_DEFAULTS} for an example)
888
889   """
890   ret_specs = {}
891   ret_policy = {}
892   for (key, val) in policy.items():
893     if key == "bounds specs":
894       ret_specs[constants.ISPECS_MINMAX] = []
895       for minmax in val:
896         ret_minmax = {}
897         for key in minmax:
898           keyparts = key.split("/", 1)
899           assert len(keyparts) > 1
900           ret_minmax[keyparts[0]] = minmax[key]
901         ret_specs[constants.ISPECS_MINMAX].append(ret_minmax)
902     elif key == constants.ISPECS_STD:
903       ret_specs[key] = val
904     else:
905       ret_policy[key] = val
906   return (ret_policy, ret_specs)