(2.9) Make NiceSort treat integers well
[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 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)