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