Preserve device names during instance move
[ganeti-local] / qa / qa_instance.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 """Instance related QA tests.
23
24 """
25
26 import operator
27 import os
28 import re
29
30 from ganeti import utils
31 from ganeti import constants
32 from ganeti import query
33 from ganeti import pathutils
34
35 import qa_config
36 import qa_utils
37 import qa_error
38
39 from qa_utils import AssertIn, AssertCommand, AssertEqual
40 from qa_utils import InstanceCheck, INST_DOWN, INST_UP, FIRST_ARG, RETURN_VALUE
41
42
43 def _GetDiskStatePath(disk):
44   return "/sys/block/%s/device/state" % disk
45
46
47 def _GetGenericAddParameters(inst, disk_template, force_mac=None):
48   params = ["-B"]
49   params.append("%s=%s,%s=%s" % (constants.BE_MINMEM,
50                                  qa_config.get(constants.BE_MINMEM),
51                                  constants.BE_MAXMEM,
52                                  qa_config.get(constants.BE_MAXMEM)))
53
54   if disk_template != constants.DT_DISKLESS:
55     for idx, disk in enumerate(qa_config.GetDiskOptions()):
56       size = disk.get("size")
57       name = disk.get("name")
58       params.extend(["--disk", "%s:size=%s,name=%s" % (idx, size, name)])
59
60   # Set static MAC address if configured
61   if force_mac:
62     nic0_mac = force_mac
63   else:
64     nic0_mac = inst.GetNicMacAddr(0, None)
65
66   if nic0_mac:
67     params.extend(["--net", "0:mac=%s" % nic0_mac])
68
69   return params
70
71
72 def _CreateInstanceByDiskTemplateRaw(nodes_spec, disk_template, fail=False):
73   """Creates an instance with the given disk template on the given nodes(s).
74      Note that this function does not check if enough nodes are given for
75      the respective disk template.
76
77   @type nodes_spec: string
78   @param nodes_spec: string specification of one node (by node name) or several
79                      nodes according to the requirements of the disk template
80   @type disk_template: string
81   @param disk_template: the disk template to be used by the instance
82   @return: the created instance
83
84   """
85   instance = qa_config.AcquireInstance()
86   try:
87     cmd = (["gnt-instance", "add",
88             "--os-type=%s" % qa_config.get("os"),
89             "--disk-template=%s" % disk_template,
90             "--node=%s" % nodes_spec] +
91            _GetGenericAddParameters(instance, disk_template))
92     cmd.append(instance.name)
93
94     AssertCommand(cmd, fail=fail)
95
96     if not fail:
97       _CheckSsconfInstanceList(instance.name)
98       instance.SetDiskTemplate(disk_template)
99
100       return instance
101   except:
102     instance.Release()
103     raise
104
105   # Handle the case where creation is expected to fail
106   assert fail
107   instance.Release()
108   return None
109
110
111 def _CreateInstanceByDiskTemplateOneNode(nodes, disk_template, fail=False):
112   """Creates an instance using the given disk template for disk templates
113      for which one given node is sufficient. These templates are for example:
114      plain, diskless, file, sharedfile, blockdev, rados.
115
116   @type nodes: list of nodes
117   @param nodes: a list of nodes, whose first element is used to create the
118                 instance
119   @type disk_template: string
120   @param disk_template: the disk template to be used by the instance
121   @return: the created instance
122
123   """
124   assert len(nodes) > 0
125   return _CreateInstanceByDiskTemplateRaw(nodes[0].primary, disk_template,
126                                           fail=fail)
127
128
129 def _CreateInstanceDrbd8(nodes, fail=False):
130   """Creates an instance using disk template 'drbd' on the given nodes.
131
132   @type nodes: list of nodes
133   @param nodes: nodes to be used by the instance
134   @return: the created instance
135
136   """
137   assert len(nodes) > 1
138   return _CreateInstanceByDiskTemplateRaw(
139     ":".join(map(operator.attrgetter("primary"), nodes)),
140     constants.DT_DRBD8, fail=fail)
141
142
143 def CreateInstanceByDiskTemplate(nodes, disk_template, fail=False):
144   """Given a disk template, this function creates an instance using
145      the template. It uses the required number of nodes depending on
146      the disk template. This function is intended to be used by tests
147      that don't care about the specifics of the instance other than
148      that it uses the given disk template.
149
150      Note: If you use this function, make sure to call
151      'TestInstanceRemove' at the end of your tests to avoid orphaned
152      instances hanging around and interfering with the following tests.
153
154   @type nodes: list of nodes
155   @param nodes: the list of the nodes on which the instance will be placed;
156                 it needs to have sufficiently many elements for the given
157                 disk template
158   @type disk_template: string
159   @param disk_template: the disk template to be used by the instance
160   @return: the created instance
161
162   """
163   if disk_template == constants.DT_DRBD8:
164     return _CreateInstanceDrbd8(nodes, fail=fail)
165   elif disk_template in [constants.DT_DISKLESS, constants.DT_PLAIN,
166                          constants.DT_FILE]:
167     return _CreateInstanceByDiskTemplateOneNode(nodes, disk_template, fail=fail)
168   else:
169     # FIXME: This assumes that for all other disk templates, we only need one
170     # node and no disk template specific parameters. This else-branch is
171     # currently only used in cases where we expect failure. Extend it when
172     # QA needs for these templates change.
173     return _CreateInstanceByDiskTemplateOneNode(nodes, disk_template, fail=fail)
174
175
176 def _GetInstanceInfo(instance):
177   """Return information about the actual state of an instance.
178
179   @type instance: string
180   @param instance: the instance name
181   @return: a dictionary with the following keys:
182       - "nodes": instance nodes, a list of strings
183       - "volumes": instance volume IDs, a list of strings
184       - "drbd-minors": DRBD minors used by the instance, a dictionary where
185         keys are nodes, and values are lists of integers (or an empty
186         dictionary for non-DRBD instances)
187       - "disk-template": instance disk template
188       - "storage-type": storage type associated with the instance disk template
189
190   """
191   node_elem = r"([^,()]+)(?:\s+\([^)]+\))?"
192   # re_nodelist matches a list of nodes returned by gnt-instance info, e.g.:
193   #  node1.fqdn
194   #  node2.fqdn,node3.fqdn
195   #  node4.fqdn (group mygroup, group UUID 01234567-abcd-0123-4567-0123456789ab)
196   # FIXME This works with no more than 2 secondaries
197   re_nodelist = re.compile(node_elem + "(?:," + node_elem + ")?$")
198
199   info = qa_utils.GetObjectInfo(["gnt-instance", "info", instance])[0]
200   nodes = []
201   for nodeinfo in info["Nodes"]:
202     if "primary" in nodeinfo:
203       nodes.append(nodeinfo["primary"])
204     elif "secondaries" in nodeinfo:
205       nodestr = nodeinfo["secondaries"]
206       if nodestr:
207         m = re_nodelist.match(nodestr)
208         if m:
209           nodes.extend(filter(None, m.groups()))
210         else:
211           nodes.append(nodestr)
212
213   disk_template = info["Disk template"]
214   if not disk_template:
215     raise qa_error.Error("Can't get instance disk template")
216   storage_type = constants.DISK_TEMPLATES_STORAGE_TYPE[disk_template]
217
218   re_drbdnode = re.compile(r"^([^\s,]+),\s+minor=([0-9]+)$")
219   vols = []
220   drbd_min = {}
221   for (count, diskinfo) in enumerate(info["Disks"]):
222     (dtype, _) = diskinfo["disk/%s" % count].split(",", 1)
223     if dtype == constants.LD_DRBD8:
224       for child in diskinfo["child devices"]:
225         vols.append(child["logical_id"])
226       for key in ["nodeA", "nodeB"]:
227         m = re_drbdnode.match(diskinfo[key])
228         if not m:
229           raise qa_error.Error("Cannot parse DRBD info: %s" % diskinfo[key])
230         node = m.group(1)
231         minor = int(m.group(2))
232         minorlist = drbd_min.setdefault(node, [])
233         minorlist.append(minor)
234     elif dtype == constants.LD_LV:
235       vols.append(diskinfo["logical_id"])
236
237   assert nodes
238   assert len(nodes) < 2 or vols
239   return {
240     "nodes": nodes,
241     "volumes": vols,
242     "drbd-minors": drbd_min,
243     "disk-template": disk_template,
244     "storage-type": storage_type,
245     }
246
247
248 def _DestroyInstanceDisks(instance):
249   """Remove all the backend disks of an instance.
250
251   This is used to simulate HW errors (dead nodes, broken disks...); the
252   configuration of the instance is not affected.
253   @type instance: dictionary
254   @param instance: the instance
255
256   """
257   info = _GetInstanceInfo(instance.name)
258   # FIXME: destruction/removal should be part of the disk class
259   if info["storage-type"] == constants.ST_LVM_VG:
260     vols = info["volumes"]
261     for node in info["nodes"]:
262       AssertCommand(["lvremove", "-f"] + vols, node=node)
263   elif info["storage-type"] == constants.ST_FILE:
264     # FIXME: file storage dir not configurable in qa
265     # Note that this works for both file and sharedfile, and this is intended.
266     filestorage = pathutils.DEFAULT_FILE_STORAGE_DIR
267     idir = os.path.join(filestorage, instance.name)
268     for node in info["nodes"]:
269       AssertCommand(["rm", "-rf", idir], node=node)
270   elif info["storage-type"] == constants.ST_DISKLESS:
271     pass
272
273
274 def _GetInstanceField(instance, field):
275   """Get the value of a field of an instance.
276
277   @type instance: string
278   @param instance: Instance name
279   @type field: string
280   @param field: Name of the field
281   @rtype: string
282
283   """
284   master = qa_config.GetMasterNode()
285   infocmd = utils.ShellQuoteArgs(["gnt-instance", "list", "--no-headers",
286                                   "--units", "m", "-o", field, instance])
287   return qa_utils.GetCommandOutput(master.primary, infocmd).strip()
288
289
290 def _GetBoolInstanceField(instance, field):
291   """Get the Boolean value of a field of an instance.
292
293   @type instance: string
294   @param instance: Instance name
295   @type field: string
296   @param field: Name of the field
297   @rtype: bool
298
299   """
300   info_out = _GetInstanceField(instance, field)
301   if info_out == "Y":
302     return True
303   elif info_out == "N":
304     return False
305   else:
306     raise qa_error.Error("Field %s of instance %s has a non-Boolean value:"
307                          " %s" % (field, instance, info_out))
308
309
310 def _GetNumInstanceField(instance, field):
311   """Get a numeric value of a field of an instance.
312
313   @type instance: string
314   @param instance: Instance name
315   @type field: string
316   @param field: Name of the field
317   @rtype: int or float
318
319   """
320   info_out = _GetInstanceField(instance, field)
321   try:
322     ret = int(info_out)
323   except ValueError:
324     try:
325       ret = float(info_out)
326     except ValueError:
327       raise qa_error.Error("Field %s of instance %s has a non-numeric value:"
328                            " %s" % (field, instance, info_out))
329   return ret
330
331
332 def GetInstanceSpec(instance, spec):
333   """Return the current spec for the given parameter.
334
335   @type instance: string
336   @param instance: Instance name
337   @type spec: string
338   @param spec: one of the supported parameters: "mem-size", "cpu-count",
339       "disk-count", "disk-size", "nic-count"
340   @rtype: tuple
341   @return: (minspec, maxspec); minspec and maxspec can be different only for
342       memory and disk size
343
344   """
345   specmap = {
346     "mem-size": ["be/minmem", "be/maxmem"],
347     "cpu-count": ["vcpus"],
348     "disk-count": ["disk.count"],
349     "disk-size": ["disk.size/ "],
350     "nic-count": ["nic.count"],
351     }
352   # For disks, first we need the number of disks
353   if spec == "disk-size":
354     (numdisk, _) = GetInstanceSpec(instance, "disk-count")
355     fields = ["disk.size/%s" % k for k in range(0, numdisk)]
356   else:
357     assert spec in specmap, "%s not in %s" % (spec, specmap)
358     fields = specmap[spec]
359   values = [_GetNumInstanceField(instance, f) for f in fields]
360   return (min(values), max(values))
361
362
363 def IsFailoverSupported(instance):
364   return instance.disk_template in constants.DTS_MIRRORED
365
366
367 def IsMigrationSupported(instance):
368   return instance.disk_template in constants.DTS_MIRRORED
369
370
371 def IsDiskReplacingSupported(instance):
372   return instance.disk_template == constants.DT_DRBD8
373
374
375 def TestInstanceAddWithPlainDisk(nodes, fail=False):
376   """gnt-instance add -t plain"""
377   if constants.DT_PLAIN in qa_config.GetEnabledDiskTemplates():
378     instance = _CreateInstanceByDiskTemplateOneNode(nodes, constants.DT_PLAIN,
379                                                     fail=fail)
380     if not fail:
381       qa_utils.RunInstanceCheck(instance, True)
382     return instance
383
384
385 @InstanceCheck(None, INST_UP, RETURN_VALUE)
386 def TestInstanceAddWithDrbdDisk(nodes):
387   """gnt-instance add -t drbd"""
388   if constants.DT_DRBD8 in qa_config.GetEnabledDiskTemplates():
389     return _CreateInstanceDrbd8(nodes)
390
391
392 @InstanceCheck(None, INST_UP, RETURN_VALUE)
393 def TestInstanceAddFile(nodes):
394   """gnt-instance add -t file"""
395   assert len(nodes) == 1
396   if constants.DT_FILE in qa_config.GetEnabledDiskTemplates():
397     return _CreateInstanceByDiskTemplateOneNode(nodes, constants.DT_FILE)
398
399
400 @InstanceCheck(None, INST_UP, RETURN_VALUE)
401 def TestInstanceAddDiskless(nodes):
402   """gnt-instance add -t diskless"""
403   assert len(nodes) == 1
404   if constants.DT_FILE in qa_config.GetEnabledDiskTemplates():
405     return _CreateInstanceByDiskTemplateOneNode(nodes, constants.DT_DISKLESS)
406
407
408 @InstanceCheck(None, INST_DOWN, FIRST_ARG)
409 def TestInstanceRemove(instance):
410   """gnt-instance remove"""
411   AssertCommand(["gnt-instance", "remove", "-f", instance.name])
412
413
414 @InstanceCheck(INST_DOWN, INST_UP, FIRST_ARG)
415 def TestInstanceStartup(instance):
416   """gnt-instance startup"""
417   AssertCommand(["gnt-instance", "startup", instance.name])
418
419
420 @InstanceCheck(INST_UP, INST_DOWN, FIRST_ARG)
421 def TestInstanceShutdown(instance):
422   """gnt-instance shutdown"""
423   AssertCommand(["gnt-instance", "shutdown", instance.name])
424
425
426 @InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
427 def TestInstanceReboot(instance):
428   """gnt-instance reboot"""
429   options = qa_config.get("options", {})
430   reboot_types = options.get("reboot-types", constants.REBOOT_TYPES)
431   name = instance.name
432   for rtype in reboot_types:
433     AssertCommand(["gnt-instance", "reboot", "--type=%s" % rtype, name])
434
435   AssertCommand(["gnt-instance", "shutdown", name])
436   qa_utils.RunInstanceCheck(instance, False)
437   AssertCommand(["gnt-instance", "reboot", name])
438
439   master = qa_config.GetMasterNode()
440   cmd = ["gnt-instance", "list", "--no-headers", "-o", "status", name]
441   result_output = qa_utils.GetCommandOutput(master.primary,
442                                             utils.ShellQuoteArgs(cmd))
443   AssertEqual(result_output.strip(), constants.INSTST_RUNNING)
444
445
446 @InstanceCheck(INST_DOWN, INST_DOWN, FIRST_ARG)
447 def TestInstanceReinstall(instance):
448   """gnt-instance reinstall"""
449   if instance.disk_template == constants.DT_DISKLESS:
450     print qa_utils.FormatInfo("Test not supported for diskless instances")
451     return
452
453   AssertCommand(["gnt-instance", "reinstall", "-f", instance.name])
454
455   # Test with non-existant OS definition
456   AssertCommand(["gnt-instance", "reinstall", "-f",
457                  "--os-type=NonExistantOsForQa",
458                  instance.name],
459                 fail=True)
460
461
462 def _ReadSsconfInstanceList():
463   """Reads ssconf_instance_list from the master node.
464
465   """
466   master = qa_config.GetMasterNode()
467
468   ssconf_path = utils.PathJoin(pathutils.DATA_DIR,
469                                "ssconf_%s" % constants.SS_INSTANCE_LIST)
470
471   cmd = ["cat", qa_utils.MakeNodePath(master, ssconf_path)]
472
473   return qa_utils.GetCommandOutput(master.primary,
474                                    utils.ShellQuoteArgs(cmd)).splitlines()
475
476
477 def _CheckSsconfInstanceList(instance):
478   """Checks if a certain instance is in the ssconf instance list.
479
480   @type instance: string
481   @param instance: Instance name
482
483   """
484   AssertIn(qa_utils.ResolveInstanceName(instance),
485            _ReadSsconfInstanceList())
486
487
488 @InstanceCheck(INST_DOWN, INST_DOWN, FIRST_ARG)
489 def TestInstanceRenameAndBack(rename_source, rename_target):
490   """gnt-instance rename
491
492   This must leave the instance with the original name, not the target
493   name.
494
495   """
496   _CheckSsconfInstanceList(rename_source)
497
498   # first do a rename to a different actual name, expecting it to fail
499   qa_utils.AddToEtcHosts(["meeeeh-not-exists", rename_target])
500   try:
501     AssertCommand(["gnt-instance", "rename", rename_source, rename_target],
502                   fail=True)
503     _CheckSsconfInstanceList(rename_source)
504   finally:
505     qa_utils.RemoveFromEtcHosts(["meeeeh-not-exists", rename_target])
506
507   info = _GetInstanceInfo(rename_source)
508
509   # Check instance volume tags correctly updated. Note that this check is lvm
510   # specific, so we skip it for non-lvm-based instances.
511   # FIXME: This will need updating when instances will be able to have
512   # different disks living on storage pools with etherogeneous storage types.
513   # FIXME: This check should be put inside the disk/storage class themselves,
514   # rather than explicitly called here.
515   if info["storage-type"] == constants.ST_LVM_VG:
516     # In the lvm world we can check for tags on the logical volume
517     tags_cmd = ("lvs -o tags --noheadings %s | grep " %
518                 (" ".join(info["volumes"]), ))
519   else:
520     # Other storage types don't have tags, so we use an always failing command,
521     # to make sure it never gets executed
522     tags_cmd = "false"
523
524   # and now rename instance to rename_target...
525   AssertCommand(["gnt-instance", "rename", rename_source, rename_target])
526   _CheckSsconfInstanceList(rename_target)
527   qa_utils.RunInstanceCheck(rename_source, False)
528   qa_utils.RunInstanceCheck(rename_target, False)
529
530   # NOTE: tags might not be the exactly as the instance name, due to
531   # charset restrictions; hence the test might be flaky
532   if (rename_source != rename_target and
533       info["storage-type"] == constants.ST_LVM_VG):
534     for node in info["nodes"]:
535       AssertCommand(tags_cmd + rename_source, node=node, fail=True)
536       AssertCommand(tags_cmd + rename_target, node=node, fail=False)
537
538   # and back
539   AssertCommand(["gnt-instance", "rename", rename_target, rename_source])
540   _CheckSsconfInstanceList(rename_source)
541   qa_utils.RunInstanceCheck(rename_target, False)
542
543   if (rename_source != rename_target and
544       info["storage-type"] == constants.ST_LVM_VG):
545     for node in info["nodes"]:
546       AssertCommand(tags_cmd + rename_source, node=node, fail=False)
547       AssertCommand(tags_cmd + rename_target, node=node, fail=True)
548
549
550 @InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
551 def TestInstanceFailover(instance):
552   """gnt-instance failover"""
553   if not IsFailoverSupported(instance):
554     print qa_utils.FormatInfo("Instance doesn't support failover, skipping"
555                               " test")
556     return
557
558   cmd = ["gnt-instance", "failover", "--force", instance.name]
559
560   # failover ...
561   AssertCommand(cmd)
562   qa_utils.RunInstanceCheck(instance, True)
563
564   # ... and back
565   AssertCommand(cmd)
566
567
568 @InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
569 def TestInstanceMigrate(instance, toggle_always_failover=True):
570   """gnt-instance migrate"""
571   if not IsMigrationSupported(instance):
572     print qa_utils.FormatInfo("Instance doesn't support migration, skipping"
573                               " test")
574     return
575
576   cmd = ["gnt-instance", "migrate", "--force", instance.name]
577   af_par = constants.BE_ALWAYS_FAILOVER
578   af_field = "be/" + constants.BE_ALWAYS_FAILOVER
579   af_init_val = _GetBoolInstanceField(instance.name, af_field)
580
581   # migrate ...
582   AssertCommand(cmd)
583   # TODO: Verify the choice between failover and migration
584   qa_utils.RunInstanceCheck(instance, True)
585
586   # ... and back (possibly with always_failover toggled)
587   if toggle_always_failover:
588     AssertCommand(["gnt-instance", "modify", "-B",
589                    ("%s=%s" % (af_par, not af_init_val)),
590                    instance.name])
591   AssertCommand(cmd)
592   # TODO: Verify the choice between failover and migration
593   qa_utils.RunInstanceCheck(instance, True)
594   if toggle_always_failover:
595     AssertCommand(["gnt-instance", "modify", "-B",
596                    ("%s=%s" % (af_par, af_init_val)), instance.name])
597
598   # TODO: Split into multiple tests
599   AssertCommand(["gnt-instance", "shutdown", instance.name])
600   qa_utils.RunInstanceCheck(instance, False)
601   AssertCommand(cmd, fail=True)
602   AssertCommand(["gnt-instance", "migrate", "--force", "--allow-failover",
603                  instance.name])
604   AssertCommand(["gnt-instance", "start", instance.name])
605   AssertCommand(cmd)
606   # @InstanceCheck enforces the check that the instance is running
607   qa_utils.RunInstanceCheck(instance, True)
608
609   AssertCommand(["gnt-instance", "modify", "-B",
610                  ("%s=%s" %
611                   (constants.BE_ALWAYS_FAILOVER, constants.VALUE_TRUE)),
612                  instance.name])
613
614   AssertCommand(cmd)
615   qa_utils.RunInstanceCheck(instance, True)
616   # TODO: Verify that a failover has been done instead of a migration
617
618   # TODO: Verify whether the default value is restored here (not hardcoded)
619   AssertCommand(["gnt-instance", "modify", "-B",
620                  ("%s=%s" %
621                   (constants.BE_ALWAYS_FAILOVER, constants.VALUE_FALSE)),
622                  instance.name])
623
624   AssertCommand(cmd)
625   qa_utils.RunInstanceCheck(instance, True)
626
627
628 def TestInstanceInfo(instance):
629   """gnt-instance info"""
630   AssertCommand(["gnt-instance", "info", instance.name])
631
632
633 @InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
634 def TestInstanceModify(instance):
635   """gnt-instance modify"""
636   default_hv = qa_config.GetDefaultHypervisor()
637
638   # Assume /sbin/init exists on all systems
639   test_kernel = "/sbin/init"
640   test_initrd = test_kernel
641
642   orig_maxmem = qa_config.get(constants.BE_MAXMEM)
643   orig_minmem = qa_config.get(constants.BE_MINMEM)
644   #orig_bridge = qa_config.get("bridge", "xen-br0")
645
646   args = [
647     ["-B", "%s=128" % constants.BE_MINMEM],
648     ["-B", "%s=128" % constants.BE_MAXMEM],
649     ["-B", "%s=%s,%s=%s" % (constants.BE_MINMEM, orig_minmem,
650                             constants.BE_MAXMEM, orig_maxmem)],
651     ["-B", "%s=2" % constants.BE_VCPUS],
652     ["-B", "%s=1" % constants.BE_VCPUS],
653     ["-B", "%s=%s" % (constants.BE_VCPUS, constants.VALUE_DEFAULT)],
654     ["-B", "%s=%s" % (constants.BE_ALWAYS_FAILOVER, constants.VALUE_TRUE)],
655     ["-B", "%s=%s" % (constants.BE_ALWAYS_FAILOVER, constants.VALUE_DEFAULT)],
656
657     ["-H", "%s=%s" % (constants.HV_KERNEL_PATH, test_kernel)],
658     ["-H", "%s=%s" % (constants.HV_KERNEL_PATH, constants.VALUE_DEFAULT)],
659
660     # TODO: bridge tests
661     #["--bridge", "xen-br1"],
662     #["--bridge", orig_bridge],
663     ]
664
665   if default_hv == constants.HT_XEN_PVM:
666     args.extend([
667       ["-H", "%s=%s" % (constants.HV_INITRD_PATH, test_initrd)],
668       ["-H", "no_%s" % (constants.HV_INITRD_PATH, )],
669       ["-H", "%s=%s" % (constants.HV_INITRD_PATH, constants.VALUE_DEFAULT)],
670       ])
671   elif default_hv == constants.HT_XEN_HVM:
672     args.extend([
673       ["-H", "%s=acn" % constants.HV_BOOT_ORDER],
674       ["-H", "%s=%s" % (constants.HV_BOOT_ORDER, constants.VALUE_DEFAULT)],
675       ])
676
677   for alist in args:
678     AssertCommand(["gnt-instance", "modify"] + alist + [instance.name])
679
680   # check no-modify
681   AssertCommand(["gnt-instance", "modify", instance.name], fail=True)
682
683   # Marking offline while instance is running must fail...
684   AssertCommand(["gnt-instance", "modify", "--offline", instance.name],
685                  fail=True)
686
687   # ...while making it online is ok, and should work
688   AssertCommand(["gnt-instance", "modify", "--online", instance.name])
689
690
691 @InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
692 def TestInstanceModifyPrimaryAndBack(instance, currentnode, othernode):
693   """gnt-instance modify --new-primary
694
695   This will leave the instance on its original primary node, not other node.
696
697   """
698   if instance.disk_template != constants.DT_FILE:
699     print qa_utils.FormatInfo("Test only supported for the file disk template")
700     return
701
702   cluster_name = qa_config.get("name")
703
704   name = instance.name
705   current = currentnode.primary
706   other = othernode.primary
707
708   # FIXME: the qa doesn't have a customizable file storage dir parameter. As
709   # such for now we use the default.
710   filestorage = pathutils.DEFAULT_FILE_STORAGE_DIR
711   disk = os.path.join(filestorage, name)
712
713   AssertCommand(["gnt-instance", "modify", "--new-primary=%s" % other, name],
714                 fail=True)
715   AssertCommand(["gnt-instance", "shutdown", name])
716   AssertCommand(["scp", "-oGlobalKnownHostsFile=%s" %
717                  pathutils.SSH_KNOWN_HOSTS_FILE,
718                  "-oCheckHostIp=no", "-oStrictHostKeyChecking=yes",
719                  "-oHashKnownHosts=no", "-oHostKeyAlias=%s" % cluster_name,
720                  "-r", disk, "%s:%s" % (other, filestorage)], node=current)
721   AssertCommand(["gnt-instance", "modify", "--new-primary=%s" % other, name])
722   AssertCommand(["gnt-instance", "startup", name])
723
724   # and back
725   AssertCommand(["gnt-instance", "shutdown", name])
726   AssertCommand(["rm", "-rf", disk], node=other)
727   AssertCommand(["gnt-instance", "modify", "--new-primary=%s" % current, name])
728   AssertCommand(["gnt-instance", "startup", name])
729
730
731 @InstanceCheck(INST_DOWN, INST_DOWN, FIRST_ARG)
732 def TestInstanceStoppedModify(instance):
733   """gnt-instance modify (stopped instance)"""
734   name = instance.name
735
736   # Instance was not marked offline; try marking it online once more
737   AssertCommand(["gnt-instance", "modify", "--online", name])
738
739   # Mark instance as offline
740   AssertCommand(["gnt-instance", "modify", "--offline", name])
741
742   # When the instance is offline shutdown should only work with --force,
743   # while start should never work
744   AssertCommand(["gnt-instance", "shutdown", name], fail=True)
745   AssertCommand(["gnt-instance", "shutdown", "--force", name])
746   AssertCommand(["gnt-instance", "start", name], fail=True)
747   AssertCommand(["gnt-instance", "start", "--force", name], fail=True)
748
749   # Also do offline to offline
750   AssertCommand(["gnt-instance", "modify", "--offline", name])
751
752   # And online again
753   AssertCommand(["gnt-instance", "modify", "--online", name])
754
755
756 @InstanceCheck(INST_DOWN, INST_DOWN, FIRST_ARG)
757 def TestInstanceConvertDiskToPlain(instance, inodes):
758   """gnt-instance modify -t"""
759   name = instance.name
760
761   template = instance.disk_template
762   if template != constants.DT_DRBD8:
763     print qa_utils.FormatInfo("Unsupported template %s, skipping conversion"
764                               " test" % template)
765     return
766
767   assert len(inodes) == 2
768   AssertCommand(["gnt-instance", "modify", "-t", constants.DT_PLAIN, name])
769   AssertCommand(["gnt-instance", "modify", "-t", constants.DT_DRBD8,
770                  "-n", inodes[1].primary, name])
771
772
773 @InstanceCheck(INST_DOWN, INST_DOWN, FIRST_ARG)
774 def TestInstanceGrowDisk(instance):
775   """gnt-instance grow-disk"""
776   if qa_config.GetExclusiveStorage():
777     print qa_utils.FormatInfo("Test not supported with exclusive_storage")
778     return
779
780   if instance.disk_template == constants.DT_DISKLESS:
781     print qa_utils.FormatInfo("Test not supported for diskless instances")
782     return
783
784   name = instance.name
785   disks = qa_config.GetDiskOptions()
786   all_size = [d.get("size") for d in disks]
787   all_grow = [d.get("growth") for d in disks]
788
789   if not all_grow:
790     # missing disk sizes but instance grow disk has been enabled,
791     # let's set fixed/nomimal growth
792     all_grow = ["128M" for _ in all_size]
793
794   for idx, (size, grow) in enumerate(zip(all_size, all_grow)):
795     # succeed in grow by amount
796     AssertCommand(["gnt-instance", "grow-disk", name, str(idx), grow])
797     # fail in grow to the old size
798     AssertCommand(["gnt-instance", "grow-disk", "--absolute", name, str(idx),
799                    size], fail=True)
800     # succeed to grow to old size + 2 * growth
801     int_size = utils.ParseUnit(size)
802     int_grow = utils.ParseUnit(grow)
803     AssertCommand(["gnt-instance", "grow-disk", "--absolute", name, str(idx),
804                    str(int_size + 2 * int_grow)])
805
806
807 @InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
808 def TestInstanceDeviceNames(instance):
809   if instance.disk_template == constants.DT_DISKLESS:
810     print qa_utils.FormatInfo("Test not supported for diskless instances")
811     return
812
813   name = instance.name
814   for dev_type in ["disk", "net"]:
815     if dev_type == "disk":
816       options = ",size=512M"
817     else:
818       options = ""
819     # succeed in adding a device named 'test_device'
820     AssertCommand(["gnt-instance", "modify",
821                    "--%s=-1:add,name=test_device%s" % (dev_type, options),
822                    name])
823     # succeed in removing the 'test_device'
824     AssertCommand(["gnt-instance", "modify",
825                    "--%s=test_device:remove" % dev_type,
826                    name])
827     # fail to add two devices with the same name
828     AssertCommand(["gnt-instance", "modify",
829                    "--%s=-1:add,name=test_device%s" % (dev_type, options),
830                    "--%s=-1:add,name=test_device%s" % (dev_type, options),
831                    name], fail=True)
832     # fail to add a device with invalid name
833     AssertCommand(["gnt-instance", "modify",
834                    "--%s=-1:add,name=2%s" % (dev_type, options),
835                    name], fail=True)
836   # Rename disks
837   disks = qa_config.GetDiskOptions()
838   disk_names = [d.get("name") for d in disks]
839   for idx, disk_name in enumerate(disk_names):
840     # Refer to disk by idx
841     AssertCommand(["gnt-instance", "modify",
842                    "--disk=%s:modify,name=renamed" % idx,
843                    name])
844     # Refer to by name and rename to original name
845     AssertCommand(["gnt-instance", "modify",
846                    "--disk=renamed:modify,name=%s" % disk_name,
847                    name])
848   if len(disks) >= 2:
849     # fail in renaming to disks to the same name
850     AssertCommand(["gnt-instance", "modify",
851                    "--disk=0:modify,name=same_name",
852                    "--disk=1:modify,name=same_name",
853                    name], fail=True)
854
855
856 def TestInstanceList():
857   """gnt-instance list"""
858   qa_utils.GenericQueryTest("gnt-instance", query.INSTANCE_FIELDS.keys())
859
860
861 def TestInstanceListFields():
862   """gnt-instance list-fields"""
863   qa_utils.GenericQueryFieldsTest("gnt-instance", query.INSTANCE_FIELDS.keys())
864
865
866 @InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
867 def TestInstanceConsole(instance):
868   """gnt-instance console"""
869   AssertCommand(["gnt-instance", "console", "--show-cmd", instance.name])
870
871
872 @InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
873 def TestReplaceDisks(instance, curr_nodes, other_nodes):
874   """gnt-instance replace-disks"""
875   def buildcmd(args):
876     cmd = ["gnt-instance", "replace-disks"]
877     cmd.extend(args)
878     cmd.append(instance.name)
879     return cmd
880
881   if not IsDiskReplacingSupported(instance):
882     print qa_utils.FormatInfo("Instance doesn't support disk replacing,"
883                               " skipping test")
884     return
885
886   # Currently all supported templates have one primary and one secondary node
887   assert len(curr_nodes) == 2
888   snode = curr_nodes[1]
889   assert len(other_nodes) == 1
890   othernode = other_nodes[0]
891
892   options = qa_config.get("options", {})
893   use_ialloc = options.get("use-iallocators", True)
894   for data in [
895     ["-p"],
896     ["-s"],
897     # A placeholder; the actual command choice depends on use_ialloc
898     None,
899     # Restore the original secondary
900     ["--new-secondary=%s" % snode.primary],
901     ]:
902     if data is None:
903       if use_ialloc:
904         data = ["-I", constants.DEFAULT_IALLOCATOR_SHORTCUT]
905       else:
906         data = ["--new-secondary=%s" % othernode.primary]
907     AssertCommand(buildcmd(data))
908
909   AssertCommand(buildcmd(["-a"]))
910   AssertCommand(["gnt-instance", "stop", instance.name])
911   AssertCommand(buildcmd(["-a"]), fail=True)
912   AssertCommand(["gnt-instance", "activate-disks", instance.name])
913   AssertCommand(["gnt-instance", "activate-disks", "--wait-for-sync",
914                  instance.name])
915   AssertCommand(buildcmd(["-a"]))
916   AssertCommand(["gnt-instance", "start", instance.name])
917
918
919 def _AssertRecreateDisks(cmdargs, instance, fail=False, check=True,
920                          destroy=True):
921   """Execute gnt-instance recreate-disks and check the result
922
923   @param cmdargs: Arguments (instance name excluded)
924   @param instance: Instance to operate on
925   @param fail: True if the command is expected to fail
926   @param check: If True and fail is False, check that the disks work
927   @prama destroy: If True, destroy the old disks first
928
929   """
930   if destroy:
931     _DestroyInstanceDisks(instance)
932   AssertCommand((["gnt-instance", "recreate-disks"] + cmdargs +
933                  [instance.name]), fail)
934   if not fail and check:
935     # Quick check that the disks are there
936     AssertCommand(["gnt-instance", "activate-disks", instance.name])
937     AssertCommand(["gnt-instance", "activate-disks", "--wait-for-sync",
938                    instance.name])
939     AssertCommand(["gnt-instance", "deactivate-disks", instance.name])
940
941
942 @InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
943 def TestRecreateDisks(instance, inodes, othernodes):
944   """gnt-instance recreate-disks
945
946   @param instance: Instance to work on
947   @param inodes: List of the current nodes of the instance
948   @param othernodes: list/tuple of nodes where to temporarily recreate disks
949
950   """
951   options = qa_config.get("options", {})
952   use_ialloc = options.get("use-iallocators", True)
953   other_seq = ":".join([n.primary for n in othernodes])
954   orig_seq = ":".join([n.primary for n in inodes])
955   # These fail because the instance is running
956   _AssertRecreateDisks(["-n", other_seq], instance, fail=True, destroy=False)
957   if use_ialloc:
958     _AssertRecreateDisks(["-I", "hail"], instance, fail=True, destroy=False)
959   else:
960     _AssertRecreateDisks(["-n", other_seq], instance, fail=True, destroy=False)
961   AssertCommand(["gnt-instance", "stop", instance.name])
962   # Disks exist: this should fail
963   _AssertRecreateDisks([], instance, fail=True, destroy=False)
964   # Recreate disks in place
965   _AssertRecreateDisks([], instance)
966   # Move disks away
967   if use_ialloc:
968     _AssertRecreateDisks(["-I", "hail"], instance)
969     # Move disks somewhere else
970     _AssertRecreateDisks(["-I", constants.DEFAULT_IALLOCATOR_SHORTCUT],
971                          instance)
972   else:
973     _AssertRecreateDisks(["-n", other_seq], instance)
974   # Move disks back
975   _AssertRecreateDisks(["-n", orig_seq], instance, check=False)
976   # This and InstanceCheck decoration check that the disks are working
977   AssertCommand(["gnt-instance", "reinstall", "-f", instance.name])
978   AssertCommand(["gnt-instance", "start", instance.name])
979
980
981 @InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
982 def TestInstanceExport(instance, node):
983   """gnt-backup export -n ..."""
984   name = instance.name
985   AssertCommand(["gnt-backup", "export", "-n", node.primary, name])
986   return qa_utils.ResolveInstanceName(name)
987
988
989 @InstanceCheck(None, INST_DOWN, FIRST_ARG)
990 def TestInstanceExportWithRemove(instance, node):
991   """gnt-backup export --remove-instance"""
992   AssertCommand(["gnt-backup", "export", "-n", node.primary,
993                  "--remove-instance", instance.name])
994
995
996 @InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
997 def TestInstanceExportNoTarget(instance):
998   """gnt-backup export (without target node, should fail)"""
999   AssertCommand(["gnt-backup", "export", instance.name], fail=True)
1000
1001
1002 @InstanceCheck(None, INST_DOWN, FIRST_ARG)
1003 def TestInstanceImport(newinst, node, expnode, name):
1004   """gnt-backup import"""
1005   templ = constants.DT_PLAIN
1006   cmd = (["gnt-backup", "import",
1007           "--disk-template=%s" % templ,
1008           "--no-ip-check",
1009           "--src-node=%s" % expnode.primary,
1010           "--src-dir=%s/%s" % (pathutils.EXPORT_DIR, name),
1011           "--node=%s" % node.primary] +
1012          _GetGenericAddParameters(newinst, templ,
1013                                   force_mac=constants.VALUE_GENERATE))
1014   cmd.append(newinst.name)
1015   AssertCommand(cmd)
1016   newinst.SetDiskTemplate(templ)
1017
1018
1019 def TestBackupList(expnode):
1020   """gnt-backup list"""
1021   AssertCommand(["gnt-backup", "list", "--node=%s" % expnode.primary])
1022
1023   qa_utils.GenericQueryTest("gnt-backup", query.EXPORT_FIELDS.keys(),
1024                             namefield=None, test_unknown=False)
1025
1026
1027 def TestBackupListFields():
1028   """gnt-backup list-fields"""
1029   qa_utils.GenericQueryFieldsTest("gnt-backup", query.EXPORT_FIELDS.keys())
1030
1031
1032 def TestRemoveInstanceOfflineNode(instance, snode, set_offline, set_online):
1033   """gnt-instance remove with an off-line node
1034
1035   @param instance: instance
1036   @param snode: secondary node, to be set offline
1037   @param set_offline: function to call to set the node off-line
1038   @param set_online: function to call to set the node on-line
1039
1040   """
1041   info = _GetInstanceInfo(instance.name)
1042   set_offline(snode)
1043   try:
1044     TestInstanceRemove(instance)
1045   finally:
1046     set_online(snode)
1047
1048   # Clean up the disks on the offline node, if necessary
1049   if instance.disk_template not in constants.DTS_EXT_MIRROR:
1050     # FIXME: abstract the cleanup inside the disks
1051     if info["storage-type"] == constants.ST_LVM_VG:
1052       for minor in info["drbd-minors"][snode.primary]:
1053         AssertCommand(["drbdsetup", str(minor), "down"], node=snode)
1054       AssertCommand(["lvremove", "-f"] + info["volumes"], node=snode)
1055     elif info["storage-type"] == constants.ST_FILE:
1056       filestorage = pathutils.DEFAULT_FILE_STORAGE_DIR
1057       disk = os.path.join(filestorage, instance.name)
1058       AssertCommand(["rm", "-rf", disk], node=snode)
1059
1060
1061 def TestInstanceCreationRestrictedByDiskTemplates():
1062   """Test adding instances for disbled disk templates."""
1063   enabled_disk_templates = qa_config.GetEnabledDiskTemplates()
1064   nodes = qa_config.AcquireManyNodes(2)
1065
1066   # Setup the cluster with the enabled_disk_templates
1067   AssertCommand(
1068     ["gnt-cluster", "modify",
1069      "--enabled-disk-template=%s" %
1070        ",".join(enabled_disk_templates)],
1071     fail=False)
1072
1073   # Test instance creation for enabled disk templates
1074   for disk_template in enabled_disk_templates:
1075     instance = CreateInstanceByDiskTemplate(nodes, disk_template, False)
1076     TestInstanceRemove(instance)
1077
1078   # Test that instance creation fails for disabled disk templates
1079   disabled_disk_templates = list(constants.DISK_TEMPLATES
1080                                  - set(enabled_disk_templates))
1081   for disk_template in disabled_disk_templates:
1082     instance = CreateInstanceByDiskTemplate(nodes, disk_template, True)
1083
1084   # Test instance creation for after disabling enabled disk templates
1085   if (len(enabled_disk_templates) > 1):
1086     # Partition the disk templates, enable them separately and check if the
1087     # disabled ones cannot be used by instances.
1088     middle = len(enabled_disk_templates) / 2
1089     templates1 = enabled_disk_templates[:middle]
1090     templates2 = enabled_disk_templates[middle:]
1091
1092     for (enabled, disabled) in [(templates1, templates2),
1093                                 (templates2, templates1)]:
1094       AssertCommand(["gnt-cluster", "modify",
1095                      "--enabled-disk-template=%s" %
1096                        ",".join(enabled)],
1097                     fail=False)
1098       for disk_template in disabled:
1099         CreateInstanceByDiskTemplate(nodes, disk_template, True)
1100   elif (len(enabled_disk_templates) == 1):
1101     # If only one disk template is enabled in the QA config, we have to enable
1102     # some of the disabled disk templates in order to test if the disabling the
1103     # only enabled disk template prohibits creating instances of that template.
1104     AssertCommand(["gnt-cluster", "modify",
1105                    "--enabled-disk-template=%s" %
1106                      ",".join(disabled_disk_templates)],
1107                   fail=False)
1108     CreateInstanceByDiskTemplate(nodes, enabled_disk_templates[0], True)
1109   else:
1110     raise qa_error.Error("Please enable at least one disk template"
1111                          " in your QA setup.")
1112
1113   # Restore initially enabled disk templates
1114   AssertCommand(["gnt-cluster", "modify",
1115                  "--enabled-disk-template=%s" %
1116                    ",".join(enabled_disk_templates)],
1117                  fail=False)