Add snap-server to the test-relevenat packages
[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=$(tempfile --mode %o --prefix gnt) && '
374          '[[ -f "${tmp}" ]] && '
375          'cat > "${tmp}" && '
376          'echo "${tmp}"') % mode
377
378   f = open(src, "r")
379   try:
380     p = subprocess.Popen(GetSSHCommand(node, cmd), shell=False, stdin=f,
381                          stdout=subprocess.PIPE)
382     AssertEqual(p.wait(), 0)
383
384     # Return temporary filename
385     return p.stdout.read().strip()
386   finally:
387     f.close()
388
389
390 def UploadData(node, data, mode=0600, filename=None):
391   """Uploads data to a node and returns the filename.
392
393   Caller needs to remove the returned file on the node when it's not needed
394   anymore.
395
396   """
397   if filename:
398     tmp = "tmp=%s" % utils.ShellQuote(filename)
399   else:
400     tmp = "tmp=$(tempfile --mode %o --prefix gnt)" % mode
401   cmd = ("%s && "
402          "[[ -f \"${tmp}\" ]] && "
403          "cat > \"${tmp}\" && "
404          "echo \"${tmp}\"") % tmp
405
406   p = subprocess.Popen(GetSSHCommand(node, cmd), shell=False,
407                        stdin=subprocess.PIPE, stdout=subprocess.PIPE)
408   p.stdin.write(data)
409   p.stdin.close()
410   AssertEqual(p.wait(), 0)
411
412   # Return temporary filename
413   return p.stdout.read().strip()
414
415
416 def BackupFile(node, path):
417   """Creates a backup of a file on the node and returns the filename.
418
419   Caller needs to remove the returned file on the node when it's not needed
420   anymore.
421
422   """
423   vpath = MakeNodePath(node, path)
424
425   cmd = ("tmp=$(tempfile --prefix .gnt --directory=$(dirname %s)) && "
426          "[[ -f \"$tmp\" ]] && "
427          "cp %s $tmp && "
428          "echo $tmp") % (utils.ShellQuote(vpath), utils.ShellQuote(vpath))
429
430   # Return temporary filename
431   result = GetCommandOutput(node, cmd).strip()
432
433   print "Backup filename: %s" % result
434
435   return result
436
437
438 def ResolveInstanceName(instance):
439   """Gets the full name of an instance.
440
441   @type instance: string
442   @param instance: Instance name
443
444   """
445   info = GetObjectInfo(["gnt-instance", "info", instance])
446   return info[0]["Instance name"]
447
448
449 def ResolveNodeName(node):
450   """Gets the full name of a node.
451
452   """
453   info = GetObjectInfo(["gnt-node", "info", node.primary])
454   return info[0]["Node name"]
455
456
457 def GetNodeInstances(node, secondaries=False):
458   """Gets a list of instances on a node.
459
460   """
461   master = qa_config.GetMasterNode()
462   node_name = ResolveNodeName(node)
463
464   # Get list of all instances
465   cmd = ["gnt-instance", "list", "--separator=:", "--no-headers",
466          "--output=name,pnode,snodes"]
467   output = GetCommandOutput(master.primary, utils.ShellQuoteArgs(cmd))
468
469   instances = []
470   for line in output.splitlines():
471     (name, pnode, snodes) = line.split(":", 2)
472     if ((not secondaries and pnode == node_name) or
473         (secondaries and node_name in snodes.split(","))):
474       instances.append(name)
475
476   return instances
477
478
479 def _SelectQueryFields(rnd, fields):
480   """Generates a list of fields for query tests.
481
482   """
483   # Create copy for shuffling
484   fields = list(fields)
485   rnd.shuffle(fields)
486
487   # Check all fields
488   yield fields
489   yield sorted(fields)
490
491   # Duplicate fields
492   yield fields + fields
493
494   # Check small groups of fields
495   while fields:
496     yield [fields.pop() for _ in range(rnd.randint(2, 10)) if fields]
497
498
499 def _List(listcmd, fields, names):
500   """Runs a list command.
501
502   """
503   master = qa_config.GetMasterNode()
504
505   cmd = [listcmd, "list", "--separator=|", "--no-headers",
506          "--output", ",".join(fields)]
507
508   if names:
509     cmd.extend(names)
510
511   return GetCommandOutput(master.primary,
512                           utils.ShellQuoteArgs(cmd)).splitlines()
513
514
515 def GenericQueryTest(cmd, fields, namefield="name", test_unknown=True):
516   """Runs a number of tests on query commands.
517
518   @param cmd: Command name
519   @param fields: List of field names
520
521   """
522   rnd = random.Random(hash(cmd))
523
524   fields = list(fields)
525   rnd.shuffle(fields)
526
527   # Test a number of field combinations
528   for testfields in _SelectQueryFields(rnd, fields):
529     AssertRedirectedCommand([cmd, "list", "--output", ",".join(testfields)])
530
531   if namefield is not None:
532     namelist_fn = compat.partial(_List, cmd, [namefield])
533
534     # When no names were requested, the list must be sorted
535     names = namelist_fn(None)
536     AssertEqual(names, utils.NiceSort(names))
537
538     # When requesting specific names, the order must be kept
539     revnames = list(reversed(names))
540     AssertEqual(namelist_fn(revnames), revnames)
541
542     randnames = list(names)
543     rnd.shuffle(randnames)
544     AssertEqual(namelist_fn(randnames), randnames)
545
546   if test_unknown:
547     # Listing unknown items must fail
548     AssertCommand([cmd, "list", "this.name.certainly.does.not.exist"],
549                   fail=True)
550
551   # Check exit code for listing unknown field
552   AssertEqual(AssertRedirectedCommand([cmd, "list",
553                                        "--output=field/does/not/exist"],
554                                       fail=True),
555               constants.EXIT_UNKNOWN_FIELD)
556
557
558 def GenericQueryFieldsTest(cmd, fields):
559   master = qa_config.GetMasterNode()
560
561   # Listing fields
562   AssertRedirectedCommand([cmd, "list-fields"])
563   AssertRedirectedCommand([cmd, "list-fields"] + fields)
564
565   # Check listed fields (all, must be sorted)
566   realcmd = [cmd, "list-fields", "--separator=|", "--no-headers"]
567   output = GetCommandOutput(master.primary,
568                             utils.ShellQuoteArgs(realcmd)).splitlines()
569   AssertEqual([line.split("|", 1)[0] for line in output],
570               utils.NiceSort(fields))
571
572   # Check exit code for listing unknown field
573   AssertEqual(AssertCommand([cmd, "list-fields", "field/does/not/exist"],
574                             fail=True),
575               constants.EXIT_UNKNOWN_FIELD)
576
577
578 def _FormatWithColor(text, seq):
579   if not seq:
580     return text
581   return "%s%s%s" % (seq, text, _RESET_SEQ)
582
583
584 FormatWarning = lambda text: _FormatWithColor(text, _WARNING_SEQ)
585 FormatError = lambda text: _FormatWithColor(text, _ERROR_SEQ)
586 FormatInfo = lambda text: _FormatWithColor(text, _INFO_SEQ)
587
588
589 def AddToEtcHosts(hostnames):
590   """Adds hostnames to /etc/hosts.
591
592   @param hostnames: List of hostnames first used A records, all other CNAMEs
593
594   """
595   master = qa_config.GetMasterNode()
596   tmp_hosts = UploadData(master.primary, "", mode=0644)
597
598   data = []
599   for localhost in ("::1", "127.0.0.1"):
600     data.append("%s %s" % (localhost, " ".join(hostnames)))
601
602   try:
603     AssertCommand("{ cat %s && echo -e '%s'; } > %s && mv %s %s" %
604                   (utils.ShellQuote(pathutils.ETC_HOSTS),
605                    "\\n".join(data),
606                    utils.ShellQuote(tmp_hosts),
607                    utils.ShellQuote(tmp_hosts),
608                    utils.ShellQuote(pathutils.ETC_HOSTS)))
609   except Exception:
610     AssertCommand(["rm", "-f", tmp_hosts])
611     raise
612
613
614 def RemoveFromEtcHosts(hostnames):
615   """Remove hostnames from /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   quoted_tmp_hosts = utils.ShellQuote(tmp_hosts)
623
624   sed_data = " ".join(hostnames)
625   try:
626     AssertCommand(("sed -e '/^\(::1\|127\.0\.0\.1\)\s\+%s/d' %s > %s"
627                    " && mv %s %s") %
628                    (sed_data, utils.ShellQuote(pathutils.ETC_HOSTS),
629                     quoted_tmp_hosts, quoted_tmp_hosts,
630                     utils.ShellQuote(pathutils.ETC_HOSTS)))
631   except Exception:
632     AssertCommand(["rm", "-f", tmp_hosts])
633     raise
634
635
636 def RunInstanceCheck(instance, running):
637   """Check if instance is running or not.
638
639   """
640   instance_name = _GetName(instance, operator.attrgetter("name"))
641
642   script = qa_config.GetInstanceCheckScript()
643   if not script:
644     return
645
646   master_node = qa_config.GetMasterNode()
647
648   # Build command to connect to master node
649   master_ssh = GetSSHCommand(master_node.primary, "--")
650
651   if running:
652     running_shellval = "1"
653     running_text = ""
654   else:
655     running_shellval = ""
656     running_text = "not "
657
658   print FormatInfo("Checking if instance '%s' is %srunning" %
659                    (instance_name, running_text))
660
661   args = [script, instance_name]
662   env = {
663     "PATH": constants.HOOKS_PATH,
664     "RUN_UUID": _RUN_UUID,
665     "MASTER_SSH": utils.ShellQuoteArgs(master_ssh),
666     "INSTANCE_NAME": instance_name,
667     "INSTANCE_RUNNING": running_shellval,
668     }
669
670   result = os.spawnve(os.P_WAIT, script, args, env)
671   if result != 0:
672     raise qa_error.Error("Instance check failed with result %s" % result)
673
674
675 def _InstanceCheckInner(expected, instarg, args, result):
676   """Helper function used by L{InstanceCheck}.
677
678   """
679   if instarg == FIRST_ARG:
680     instance = args[0]
681   elif instarg == RETURN_VALUE:
682     instance = result
683   else:
684     raise Exception("Invalid value '%s' for instance argument" % instarg)
685
686   if expected in (INST_DOWN, INST_UP):
687     RunInstanceCheck(instance, (expected == INST_UP))
688   elif expected is not None:
689     raise Exception("Invalid value '%s'" % expected)
690
691
692 def InstanceCheck(before, after, instarg):
693   """Decorator to check instance status before and after test.
694
695   @param before: L{INST_DOWN} if instance must be stopped before test,
696     L{INST_UP} if instance must be running before test, L{None} to not check.
697   @param after: L{INST_DOWN} if instance must be stopped after test,
698     L{INST_UP} if instance must be running after test, L{None} to not check.
699   @param instarg: L{FIRST_ARG} to use first argument to test as instance (a
700     dictionary), L{RETURN_VALUE} to use return value (disallows pre-checks)
701
702   """
703   def decorator(fn):
704     @functools.wraps(fn)
705     def wrapper(*args, **kwargs):
706       _InstanceCheckInner(before, instarg, args, NotImplemented)
707
708       result = fn(*args, **kwargs)
709
710       _InstanceCheckInner(after, instarg, args, result)
711
712       return result
713     return wrapper
714   return decorator
715
716
717 def GetNonexistentGroups(count):
718   """Gets group names which shouldn't exist on the cluster.
719
720   @param count: Number of groups to get
721   @rtype: integer
722
723   """
724   return GetNonexistentEntityNames(count, "groups", "group")
725
726
727 def GetNonexistentEntityNames(count, name_config, name_prefix):
728   """Gets entity names which shouldn't exist on the cluster.
729
730   The actualy names can refer to arbitrary entities (for example
731   groups, networks).
732
733   @param count: Number of names to get
734   @rtype: integer
735   @param name_config: name of the leaf in the config containing
736     this entity's configuration, including a 'inexistent-'
737     element
738   @rtype: string
739   @param name_prefix: prefix of the entity's names, used to compose
740     the default values; for example for groups, the prefix is
741     'group' and the generated names are then group1, group2, ...
742   @rtype: string
743
744   """
745   entities = qa_config.get(name_config, {})
746
747   default = [name_prefix + str(i) for i in range(count)]
748   assert count <= len(default)
749
750   name_config_inexistent = "inexistent-" + name_config
751   candidates = entities.get(name_config_inexistent, default)[:count]
752
753   if len(candidates) < count:
754     raise Exception("At least %s non-existent %s are needed" %
755                     (count, name_config))
756
757   return candidates
758
759
760 def MakeNodePath(node, path):
761   """Builds an absolute path for a virtual node.
762
763   @type node: string or L{qa_config._QaNode}
764   @param node: Node
765   @type path: string
766   @param path: Path without node-specific prefix
767
768   """
769   (_, basedir) = qa_config.GetVclusterSettings()
770
771   if isinstance(node, basestring):
772     name = node
773   else:
774     name = node.primary
775
776   if basedir:
777     assert path.startswith("/")
778     return "%s%s" % (vcluster.MakeNodeRoot(basedir, name), path)
779   else:
780     return path
781
782
783 def _GetParameterOptions(specs):
784   """Helper to build policy options."""
785   values = ["%s=%s" % (par, val)
786             for (par, val) in specs.items()]
787   return ",".join(values)
788
789
790 def TestSetISpecs(new_specs=None, diff_specs=None, get_policy_fn=None,
791                   build_cmd_fn=None, fail=False, old_values=None):
792   """Change instance specs for an object.
793
794   At most one of new_specs or diff_specs can be specified.
795
796   @type new_specs: dict
797   @param new_specs: new complete specs, in the same format returned by
798       L{ParseIPolicy}.
799   @type diff_specs: dict
800   @param diff_specs: partial specs, it can be an incomplete specifications, but
801       if min/max specs are specified, their number must match the number of the
802       existing specs
803   @type get_policy_fn: function
804   @param get_policy_fn: function that returns the current policy as in
805       L{ParseIPolicy}
806   @type build_cmd_fn: function
807   @param build_cmd_fn: function that return the full command line from the
808       options alone
809   @type fail: bool
810   @param fail: if the change is expected to fail
811   @type old_values: tuple
812   @param old_values: (old_policy, old_specs), as returned by
813      L{ParseIPolicy}
814   @return: same as L{ParseIPolicy}
815
816   """
817   assert get_policy_fn is not None
818   assert build_cmd_fn is not None
819   assert new_specs is None or diff_specs is None
820
821   if old_values:
822     (old_policy, old_specs) = old_values
823   else:
824     (old_policy, old_specs) = get_policy_fn()
825
826   if diff_specs:
827     new_specs = copy.deepcopy(old_specs)
828     if constants.ISPECS_MINMAX in diff_specs:
829       AssertEqual(len(new_specs[constants.ISPECS_MINMAX]),
830                   len(diff_specs[constants.ISPECS_MINMAX]))
831       for (new_minmax, diff_minmax) in zip(new_specs[constants.ISPECS_MINMAX],
832                                            diff_specs[constants.ISPECS_MINMAX]):
833         for (key, parvals) in diff_minmax.items():
834           for (par, val) in parvals.items():
835             new_minmax[key][par] = val
836     for (par, val) in diff_specs.get(constants.ISPECS_STD, {}).items():
837       new_specs[constants.ISPECS_STD][par] = val
838
839   if new_specs:
840     cmd = []
841     if (diff_specs is None or constants.ISPECS_MINMAX in diff_specs):
842       minmax_opt_items = []
843       for minmax in new_specs[constants.ISPECS_MINMAX]:
844         minmax_opts = []
845         for key in ["min", "max"]:
846           keyopt = _GetParameterOptions(minmax[key])
847           minmax_opts.append("%s:%s" % (key, keyopt))
848         minmax_opt_items.append("/".join(minmax_opts))
849       cmd.extend([
850         "--ipolicy-bounds-specs",
851         "//".join(minmax_opt_items)
852         ])
853     if diff_specs is None:
854       std_source = new_specs
855     else:
856       std_source = diff_specs
857     std_opt = _GetParameterOptions(std_source.get("std", {}))
858     if std_opt:
859       cmd.extend(["--ipolicy-std-specs", std_opt])
860     AssertCommand(build_cmd_fn(cmd), fail=fail)
861
862     # Check the new state
863     (eff_policy, eff_specs) = get_policy_fn()
864     AssertEqual(eff_policy, old_policy)
865     if fail:
866       AssertEqual(eff_specs, old_specs)
867     else:
868       AssertEqual(eff_specs, new_specs)
869
870   else:
871     (eff_policy, eff_specs) = (old_policy, old_specs)
872
873   return (eff_policy, eff_specs)
874
875
876 def ParseIPolicy(policy):
877   """Parse and split instance an instance policy.
878
879   @type policy: dict
880   @param policy: policy, as returned by L{GetObjectInfo}
881   @rtype: tuple
882   @return: (policy, specs), where:
883       - policy is a dictionary of the policy values, instance specs excluded
884       - specs is a dictionary containing only the specs, using the internal
885         format (see L{constants.IPOLICY_DEFAULTS} for an example)
886
887   """
888   ret_specs = {}
889   ret_policy = {}
890   for (key, val) in policy.items():
891     if key == "bounds specs":
892       ret_specs[constants.ISPECS_MINMAX] = []
893       for minmax in val:
894         ret_minmax = {}
895         for key in minmax:
896           keyparts = key.split("/", 1)
897           assert len(keyparts) > 1
898           ret_minmax[keyparts[0]] = minmax[key]
899         ret_specs[constants.ISPECS_MINMAX].append(ret_minmax)
900     elif key == constants.ISPECS_STD:
901       ret_specs[key] = val
902     else:
903       ret_policy[key] = val
904   return (ret_policy, ret_specs)