QA: Make Query tests test for '?' values
[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 _AssertListNoUnknownValues(listcmd, fields):
518   """Assert that the list command does not output unknown values.
519
520   """
521   master = qa_config.GetMasterNode()
522
523   cmd = [listcmd, "list", "--output", ",".join(fields)]
524
525   output = GetCommandOutput(master.primary,
526                             utils.ShellQuoteArgs(cmd)).splitlines()
527   for line in output:
528     AssertNotIn('?', line)
529
530
531 def GenericQueryTest(cmd, fields, namefield="name", test_unknown=True,
532                      test_values_unknown=True):
533   """Runs a number of tests on query commands.
534
535   @param cmd: Command name
536   @param fields: List of field names
537
538   """
539   rnd = random.Random(hash(cmd))
540
541   fields = list(fields)
542   rnd.shuffle(fields)
543
544   # Test a number of field combinations
545   for testfields in _SelectQueryFields(rnd, fields):
546     AssertRedirectedCommand([cmd, "list", "--output", ",".join(testfields)])
547
548   if namefield is not None:
549     namelist_fn = compat.partial(_List, cmd, [namefield])
550
551     # When no names were requested, the list must be sorted
552     names = namelist_fn(None)
553     AssertEqual(names, utils.NiceSort(names))
554
555     # When requesting specific names, the order must be kept
556     revnames = list(reversed(names))
557     AssertEqual(namelist_fn(revnames), revnames)
558
559     randnames = list(names)
560     rnd.shuffle(randnames)
561     AssertEqual(namelist_fn(randnames), randnames)
562
563   if test_unknown:
564     # Listing unknown items must fail
565     AssertCommand([cmd, "list", "this.name.certainly.does.not.exist"],
566                   fail=True)
567
568   if test_values_unknown:
569     _AssertListNoUnknownValues(cmd, fields)
570
571   # Check exit code for listing unknown field
572   AssertEqual(AssertRedirectedCommand([cmd, "list",
573                                        "--output=field/does/not/exist"],
574                                       fail=True),
575               constants.EXIT_UNKNOWN_FIELD)
576
577
578 def GenericQueryFieldsTest(cmd, fields):
579   master = qa_config.GetMasterNode()
580
581   # Listing fields
582   AssertRedirectedCommand([cmd, "list-fields"])
583   AssertRedirectedCommand([cmd, "list-fields"] + fields)
584
585   # Check listed fields (all, must be sorted)
586   realcmd = [cmd, "list-fields", "--separator=|", "--no-headers"]
587   output = GetCommandOutput(master.primary,
588                             utils.ShellQuoteArgs(realcmd)).splitlines()
589   AssertEqual([line.split("|", 1)[0] for line in output],
590               utils.NiceSort(fields))
591
592   # Check exit code for listing unknown field
593   AssertEqual(AssertCommand([cmd, "list-fields", "field/does/not/exist"],
594                             fail=True),
595               constants.EXIT_UNKNOWN_FIELD)
596
597
598 def _FormatWithColor(text, seq):
599   if not seq:
600     return text
601   return "%s%s%s" % (seq, text, _RESET_SEQ)
602
603
604 FormatWarning = lambda text: _FormatWithColor(text, _WARNING_SEQ)
605 FormatError = lambda text: _FormatWithColor(text, _ERROR_SEQ)
606 FormatInfo = lambda text: _FormatWithColor(text, _INFO_SEQ)
607
608
609 def AddToEtcHosts(hostnames):
610   """Adds hostnames to /etc/hosts.
611
612   @param hostnames: List of hostnames first used A records, all other CNAMEs
613
614   """
615   master = qa_config.GetMasterNode()
616   tmp_hosts = UploadData(master.primary, "", mode=0644)
617
618   data = []
619   for localhost in ("::1", "127.0.0.1"):
620     data.append("%s %s" % (localhost, " ".join(hostnames)))
621
622   try:
623     AssertCommand("{ cat %s && echo -e '%s'; } > %s && mv %s %s" %
624                   (utils.ShellQuote(pathutils.ETC_HOSTS),
625                    "\\n".join(data),
626                    utils.ShellQuote(tmp_hosts),
627                    utils.ShellQuote(tmp_hosts),
628                    utils.ShellQuote(pathutils.ETC_HOSTS)))
629   except Exception:
630     AssertCommand(["rm", "-f", tmp_hosts])
631     raise
632
633
634 def RemoveFromEtcHosts(hostnames):
635   """Remove hostnames from /etc/hosts.
636
637   @param hostnames: List of hostnames first used A records, all other CNAMEs
638
639   """
640   master = qa_config.GetMasterNode()
641   tmp_hosts = UploadData(master.primary, "", mode=0644)
642   quoted_tmp_hosts = utils.ShellQuote(tmp_hosts)
643
644   sed_data = " ".join(hostnames)
645   try:
646     AssertCommand(("sed -e '/^\(::1\|127\.0\.0\.1\)\s\+%s/d' %s > %s"
647                    " && mv %s %s") %
648                    (sed_data, utils.ShellQuote(pathutils.ETC_HOSTS),
649                     quoted_tmp_hosts, quoted_tmp_hosts,
650                     utils.ShellQuote(pathutils.ETC_HOSTS)))
651   except Exception:
652     AssertCommand(["rm", "-f", tmp_hosts])
653     raise
654
655
656 def RunInstanceCheck(instance, running):
657   """Check if instance is running or not.
658
659   """
660   instance_name = _GetName(instance, operator.attrgetter("name"))
661
662   script = qa_config.GetInstanceCheckScript()
663   if not script:
664     return
665
666   master_node = qa_config.GetMasterNode()
667
668   # Build command to connect to master node
669   master_ssh = GetSSHCommand(master_node.primary, "--")
670
671   if running:
672     running_shellval = "1"
673     running_text = ""
674   else:
675     running_shellval = ""
676     running_text = "not "
677
678   print FormatInfo("Checking if instance '%s' is %srunning" %
679                    (instance_name, running_text))
680
681   args = [script, instance_name]
682   env = {
683     "PATH": constants.HOOKS_PATH,
684     "RUN_UUID": _RUN_UUID,
685     "MASTER_SSH": utils.ShellQuoteArgs(master_ssh),
686     "INSTANCE_NAME": instance_name,
687     "INSTANCE_RUNNING": running_shellval,
688     }
689
690   result = os.spawnve(os.P_WAIT, script, args, env)
691   if result != 0:
692     raise qa_error.Error("Instance check failed with result %s" % result)
693
694
695 def _InstanceCheckInner(expected, instarg, args, result):
696   """Helper function used by L{InstanceCheck}.
697
698   """
699   if instarg == FIRST_ARG:
700     instance = args[0]
701   elif instarg == RETURN_VALUE:
702     instance = result
703   else:
704     raise Exception("Invalid value '%s' for instance argument" % instarg)
705
706   if expected in (INST_DOWN, INST_UP):
707     RunInstanceCheck(instance, (expected == INST_UP))
708   elif expected is not None:
709     raise Exception("Invalid value '%s'" % expected)
710
711
712 def InstanceCheck(before, after, instarg):
713   """Decorator to check instance status before and after test.
714
715   @param before: L{INST_DOWN} if instance must be stopped before test,
716     L{INST_UP} if instance must be running before test, L{None} to not check.
717   @param after: L{INST_DOWN} if instance must be stopped after test,
718     L{INST_UP} if instance must be running after test, L{None} to not check.
719   @param instarg: L{FIRST_ARG} to use first argument to test as instance (a
720     dictionary), L{RETURN_VALUE} to use return value (disallows pre-checks)
721
722   """
723   def decorator(fn):
724     @functools.wraps(fn)
725     def wrapper(*args, **kwargs):
726       _InstanceCheckInner(before, instarg, args, NotImplemented)
727
728       result = fn(*args, **kwargs)
729
730       _InstanceCheckInner(after, instarg, args, result)
731
732       return result
733     return wrapper
734   return decorator
735
736
737 def GetNonexistentGroups(count):
738   """Gets group names which shouldn't exist on the cluster.
739
740   @param count: Number of groups to get
741   @rtype: integer
742
743   """
744   return GetNonexistentEntityNames(count, "groups", "group")
745
746
747 def GetNonexistentEntityNames(count, name_config, name_prefix):
748   """Gets entity names which shouldn't exist on the cluster.
749
750   The actualy names can refer to arbitrary entities (for example
751   groups, networks).
752
753   @param count: Number of names to get
754   @rtype: integer
755   @param name_config: name of the leaf in the config containing
756     this entity's configuration, including a 'inexistent-'
757     element
758   @rtype: string
759   @param name_prefix: prefix of the entity's names, used to compose
760     the default values; for example for groups, the prefix is
761     'group' and the generated names are then group1, group2, ...
762   @rtype: string
763
764   """
765   entities = qa_config.get(name_config, {})
766
767   default = [name_prefix + str(i) for i in range(count)]
768   assert count <= len(default)
769
770   name_config_inexistent = "inexistent-" + name_config
771   candidates = entities.get(name_config_inexistent, default)[:count]
772
773   if len(candidates) < count:
774     raise Exception("At least %s non-existent %s are needed" %
775                     (count, name_config))
776
777   return candidates
778
779
780 def MakeNodePath(node, path):
781   """Builds an absolute path for a virtual node.
782
783   @type node: string or L{qa_config._QaNode}
784   @param node: Node
785   @type path: string
786   @param path: Path without node-specific prefix
787
788   """
789   (_, basedir) = qa_config.GetVclusterSettings()
790
791   if isinstance(node, basestring):
792     name = node
793   else:
794     name = node.primary
795
796   if basedir:
797     assert path.startswith("/")
798     return "%s%s" % (vcluster.MakeNodeRoot(basedir, name), path)
799   else:
800     return path
801
802
803 def _GetParameterOptions(specs):
804   """Helper to build policy options."""
805   values = ["%s=%s" % (par, val)
806             for (par, val) in specs.items()]
807   return ",".join(values)
808
809
810 def TestSetISpecs(new_specs=None, diff_specs=None, get_policy_fn=None,
811                   build_cmd_fn=None, fail=False, old_values=None):
812   """Change instance specs for an object.
813
814   At most one of new_specs or diff_specs can be specified.
815
816   @type new_specs: dict
817   @param new_specs: new complete specs, in the same format returned by
818       L{ParseIPolicy}.
819   @type diff_specs: dict
820   @param diff_specs: partial specs, it can be an incomplete specifications, but
821       if min/max specs are specified, their number must match the number of the
822       existing specs
823   @type get_policy_fn: function
824   @param get_policy_fn: function that returns the current policy as in
825       L{ParseIPolicy}
826   @type build_cmd_fn: function
827   @param build_cmd_fn: function that return the full command line from the
828       options alone
829   @type fail: bool
830   @param fail: if the change is expected to fail
831   @type old_values: tuple
832   @param old_values: (old_policy, old_specs), as returned by
833      L{ParseIPolicy}
834   @return: same as L{ParseIPolicy}
835
836   """
837   assert get_policy_fn is not None
838   assert build_cmd_fn is not None
839   assert new_specs is None or diff_specs is None
840
841   if old_values:
842     (old_policy, old_specs) = old_values
843   else:
844     (old_policy, old_specs) = get_policy_fn()
845
846   if diff_specs:
847     new_specs = copy.deepcopy(old_specs)
848     if constants.ISPECS_MINMAX in diff_specs:
849       AssertEqual(len(new_specs[constants.ISPECS_MINMAX]),
850                   len(diff_specs[constants.ISPECS_MINMAX]))
851       for (new_minmax, diff_minmax) in zip(new_specs[constants.ISPECS_MINMAX],
852                                            diff_specs[constants.ISPECS_MINMAX]):
853         for (key, parvals) in diff_minmax.items():
854           for (par, val) in parvals.items():
855             new_minmax[key][par] = val
856     for (par, val) in diff_specs.get(constants.ISPECS_STD, {}).items():
857       new_specs[constants.ISPECS_STD][par] = val
858
859   if new_specs:
860     cmd = []
861     if (diff_specs is None or constants.ISPECS_MINMAX in diff_specs):
862       minmax_opt_items = []
863       for minmax in new_specs[constants.ISPECS_MINMAX]:
864         minmax_opts = []
865         for key in ["min", "max"]:
866           keyopt = _GetParameterOptions(minmax[key])
867           minmax_opts.append("%s:%s" % (key, keyopt))
868         minmax_opt_items.append("/".join(minmax_opts))
869       cmd.extend([
870         "--ipolicy-bounds-specs",
871         "//".join(minmax_opt_items)
872         ])
873     if diff_specs is None:
874       std_source = new_specs
875     else:
876       std_source = diff_specs
877     std_opt = _GetParameterOptions(std_source.get("std", {}))
878     if std_opt:
879       cmd.extend(["--ipolicy-std-specs", std_opt])
880     AssertCommand(build_cmd_fn(cmd), fail=fail)
881
882     # Check the new state
883     (eff_policy, eff_specs) = get_policy_fn()
884     AssertEqual(eff_policy, old_policy)
885     if fail:
886       AssertEqual(eff_specs, old_specs)
887     else:
888       AssertEqual(eff_specs, new_specs)
889
890   else:
891     (eff_policy, eff_specs) = (old_policy, old_specs)
892
893   return (eff_policy, eff_specs)
894
895
896 def ParseIPolicy(policy):
897   """Parse and split instance an instance policy.
898
899   @type policy: dict
900   @param policy: policy, as returned by L{GetObjectInfo}
901   @rtype: tuple
902   @return: (policy, specs), where:
903       - policy is a dictionary of the policy values, instance specs excluded
904       - specs is a dictionary containing only the specs, using the internal
905         format (see L{constants.IPOLICY_DEFAULTS} for an example)
906
907   """
908   ret_specs = {}
909   ret_policy = {}
910   for (key, val) in policy.items():
911     if key == "bounds specs":
912       ret_specs[constants.ISPECS_MINMAX] = []
913       for minmax in val:
914         ret_minmax = {}
915         for key in minmax:
916           keyparts = key.split("/", 1)
917           assert len(keyparts) > 1
918           ret_minmax[keyparts[0]] = minmax[key]
919         ret_specs[constants.ISPECS_MINMAX].append(ret_minmax)
920     elif key == constants.ISPECS_STD:
921       ret_specs[key] = val
922     else:
923       ret_policy[key] = val
924   return (ret_policy, ret_specs)