Extend qa to test hotplug support
[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.MAP_DISK_TEMPLATE_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.DT_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.DT_PLAIN:
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     # Note that this works for both file and sharedfile, and this is intended.
140     storage_dir = qa_config.get("file-storage-dir",
141                                 pathutils.DEFAULT_FILE_STORAGE_DIR)
142     idir = os.path.join(storage_dir, 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 TestInstanceAddSharedFile(nodes):
281   """gnt-instance add -t sharedfile"""
282   assert len(nodes) == 1
283   if constants.DT_SHARED_FILE in qa_config.GetEnabledDiskTemplates():
284     return CreateInstanceByDiskTemplateOneNode(nodes, constants.DT_SHARED_FILE)
285
286
287 @InstanceCheck(None, INST_UP, RETURN_VALUE)
288 def TestInstanceAddDiskless(nodes):
289   """gnt-instance add -t diskless"""
290   assert len(nodes) == 1
291   if constants.DT_DISKLESS in qa_config.GetEnabledDiskTemplates():
292     return CreateInstanceByDiskTemplateOneNode(nodes, constants.DT_DISKLESS)
293
294
295 @InstanceCheck(None, INST_DOWN, FIRST_ARG)
296 def TestInstanceRemove(instance):
297   """gnt-instance remove"""
298   AssertCommand(["gnt-instance", "remove", "-f", instance.name])
299
300
301 @InstanceCheck(INST_DOWN, INST_UP, FIRST_ARG)
302 def TestInstanceStartup(instance):
303   """gnt-instance startup"""
304   AssertCommand(["gnt-instance", "startup", instance.name])
305
306
307 @InstanceCheck(INST_UP, INST_DOWN, FIRST_ARG)
308 def TestInstanceShutdown(instance):
309   """gnt-instance shutdown"""
310   AssertCommand(["gnt-instance", "shutdown", instance.name])
311
312
313 @InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
314 def TestInstanceReboot(instance):
315   """gnt-instance reboot"""
316   options = qa_config.get("options", {})
317   reboot_types = options.get("reboot-types", constants.REBOOT_TYPES)
318   name = instance.name
319   for rtype in reboot_types:
320     AssertCommand(["gnt-instance", "reboot", "--type=%s" % rtype, name])
321
322   AssertCommand(["gnt-instance", "shutdown", name])
323   qa_utils.RunInstanceCheck(instance, False)
324   AssertCommand(["gnt-instance", "reboot", name])
325
326   master = qa_config.GetMasterNode()
327   cmd = ["gnt-instance", "list", "--no-headers", "-o", "status", name]
328   result_output = qa_utils.GetCommandOutput(master.primary,
329                                             utils.ShellQuoteArgs(cmd))
330   AssertEqual(result_output.strip(), constants.INSTST_RUNNING)
331
332
333 @InstanceCheck(INST_DOWN, INST_DOWN, FIRST_ARG)
334 def TestInstanceReinstall(instance):
335   """gnt-instance reinstall"""
336   if instance.disk_template == constants.DT_DISKLESS:
337     print qa_utils.FormatInfo("Test not supported for diskless instances")
338     return
339
340   AssertCommand(["gnt-instance", "reinstall", "-f", instance.name])
341
342   # Test with non-existant OS definition
343   AssertCommand(["gnt-instance", "reinstall", "-f",
344                  "--os-type=NonExistantOsForQa",
345                  instance.name],
346                 fail=True)
347
348
349 @InstanceCheck(INST_DOWN, INST_DOWN, FIRST_ARG)
350 def TestInstanceRenameAndBack(rename_source, rename_target):
351   """gnt-instance rename
352
353   This must leave the instance with the original name, not the target
354   name.
355
356   """
357   CheckSsconfInstanceList(rename_source)
358
359   # first do a rename to a different actual name, expecting it to fail
360   qa_utils.AddToEtcHosts(["meeeeh-not-exists", rename_target])
361   try:
362     AssertCommand(["gnt-instance", "rename", rename_source, rename_target],
363                   fail=True)
364     CheckSsconfInstanceList(rename_source)
365   finally:
366     qa_utils.RemoveFromEtcHosts(["meeeeh-not-exists", rename_target])
367
368   info = GetInstanceInfo(rename_source)
369
370   # Check instance volume tags correctly updated. Note that this check is lvm
371   # specific, so we skip it for non-lvm-based instances.
372   # FIXME: This will need updating when instances will be able to have
373   # different disks living on storage pools with etherogeneous storage types.
374   # FIXME: This check should be put inside the disk/storage class themselves,
375   # rather than explicitly called here.
376   if info["storage-type"] == constants.ST_LVM_VG:
377     # In the lvm world we can check for tags on the logical volume
378     tags_cmd = ("lvs -o tags --noheadings %s | grep " %
379                 (" ".join(info["volumes"]), ))
380   else:
381     # Other storage types don't have tags, so we use an always failing command,
382     # to make sure it never gets executed
383     tags_cmd = "false"
384
385   # and now rename instance to rename_target...
386   AssertCommand(["gnt-instance", "rename", rename_source, rename_target])
387   CheckSsconfInstanceList(rename_target)
388   qa_utils.RunInstanceCheck(rename_source, False)
389   qa_utils.RunInstanceCheck(rename_target, False)
390
391   # NOTE: tags might not be the exactly as the instance name, due to
392   # charset restrictions; hence the test might be flaky
393   if (rename_source != rename_target and
394       info["storage-type"] == constants.ST_LVM_VG):
395     for node in info["nodes"]:
396       AssertCommand(tags_cmd + rename_source, node=node, fail=True)
397       AssertCommand(tags_cmd + rename_target, node=node, fail=False)
398
399   # and back
400   AssertCommand(["gnt-instance", "rename", rename_target, rename_source])
401   CheckSsconfInstanceList(rename_source)
402   qa_utils.RunInstanceCheck(rename_target, False)
403
404   if (rename_source != rename_target and
405       info["storage-type"] == constants.ST_LVM_VG):
406     for node in info["nodes"]:
407       AssertCommand(tags_cmd + rename_source, node=node, fail=False)
408       AssertCommand(tags_cmd + rename_target, node=node, fail=True)
409
410
411 @InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
412 def TestInstanceFailover(instance):
413   """gnt-instance failover"""
414   if not IsFailoverSupported(instance):
415     print qa_utils.FormatInfo("Instance doesn't support failover, skipping"
416                               " test")
417     return
418
419   cmd = ["gnt-instance", "failover", "--force", instance.name]
420
421   # failover ...
422   AssertCommand(cmd)
423   qa_utils.RunInstanceCheck(instance, True)
424
425   # ... and back
426   AssertCommand(cmd)
427
428
429 @InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
430 def TestInstanceMigrate(instance, toggle_always_failover=True):
431   """gnt-instance migrate"""
432   if not IsMigrationSupported(instance):
433     print qa_utils.FormatInfo("Instance doesn't support migration, skipping"
434                               " test")
435     return
436
437   cmd = ["gnt-instance", "migrate", "--force", instance.name]
438   af_par = constants.BE_ALWAYS_FAILOVER
439   af_field = "be/" + constants.BE_ALWAYS_FAILOVER
440   af_init_val = _GetBoolInstanceField(instance.name, af_field)
441
442   # migrate ...
443   AssertCommand(cmd)
444   # TODO: Verify the choice between failover and migration
445   qa_utils.RunInstanceCheck(instance, True)
446
447   # ... and back (possibly with always_failover toggled)
448   if toggle_always_failover:
449     AssertCommand(["gnt-instance", "modify", "-B",
450                    ("%s=%s" % (af_par, not af_init_val)),
451                    instance.name])
452   AssertCommand(cmd)
453   # TODO: Verify the choice between failover and migration
454   qa_utils.RunInstanceCheck(instance, True)
455   if toggle_always_failover:
456     AssertCommand(["gnt-instance", "modify", "-B",
457                    ("%s=%s" % (af_par, af_init_val)), instance.name])
458
459   # TODO: Split into multiple tests
460   AssertCommand(["gnt-instance", "shutdown", instance.name])
461   qa_utils.RunInstanceCheck(instance, False)
462   AssertCommand(cmd, fail=True)
463   AssertCommand(["gnt-instance", "migrate", "--force", "--allow-failover",
464                  instance.name])
465   AssertCommand(["gnt-instance", "start", instance.name])
466   AssertCommand(cmd)
467   # @InstanceCheck enforces the check that the instance is running
468   qa_utils.RunInstanceCheck(instance, True)
469
470   AssertCommand(["gnt-instance", "modify", "-B",
471                  ("%s=%s" %
472                   (constants.BE_ALWAYS_FAILOVER, constants.VALUE_TRUE)),
473                  instance.name])
474
475   AssertCommand(cmd)
476   qa_utils.RunInstanceCheck(instance, True)
477   # TODO: Verify that a failover has been done instead of a migration
478
479   # TODO: Verify whether the default value is restored here (not hardcoded)
480   AssertCommand(["gnt-instance", "modify", "-B",
481                  ("%s=%s" %
482                   (constants.BE_ALWAYS_FAILOVER, constants.VALUE_FALSE)),
483                  instance.name])
484
485   AssertCommand(cmd)
486   qa_utils.RunInstanceCheck(instance, True)
487
488
489 def TestInstanceInfo(instance):
490   """gnt-instance info"""
491   AssertCommand(["gnt-instance", "info", instance.name])
492
493
494 @InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
495 def TestInstanceModify(instance):
496   """gnt-instance modify"""
497   default_hv = qa_config.GetDefaultHypervisor()
498
499   # Assume /sbin/init exists on all systems
500   test_kernel = "/sbin/init"
501   test_initrd = test_kernel
502
503   orig_maxmem = qa_config.get(constants.BE_MAXMEM)
504   orig_minmem = qa_config.get(constants.BE_MINMEM)
505   #orig_bridge = qa_config.get("bridge", "xen-br0")
506
507   args = [
508     ["-B", "%s=128" % constants.BE_MINMEM],
509     ["-B", "%s=128" % constants.BE_MAXMEM],
510     ["-B", "%s=%s,%s=%s" % (constants.BE_MINMEM, orig_minmem,
511                             constants.BE_MAXMEM, orig_maxmem)],
512     ["-B", "%s=2" % constants.BE_VCPUS],
513     ["-B", "%s=1" % constants.BE_VCPUS],
514     ["-B", "%s=%s" % (constants.BE_VCPUS, constants.VALUE_DEFAULT)],
515     ["-B", "%s=%s" % (constants.BE_ALWAYS_FAILOVER, constants.VALUE_TRUE)],
516     ["-B", "%s=%s" % (constants.BE_ALWAYS_FAILOVER, constants.VALUE_DEFAULT)],
517
518     ["-H", "%s=%s" % (constants.HV_KERNEL_PATH, test_kernel)],
519     ["-H", "%s=%s" % (constants.HV_KERNEL_PATH, constants.VALUE_DEFAULT)],
520
521     # TODO: bridge tests
522     #["--bridge", "xen-br1"],
523     #["--bridge", orig_bridge],
524     ]
525
526   if default_hv == constants.HT_XEN_PVM:
527     args.extend([
528       ["-H", "%s=%s" % (constants.HV_INITRD_PATH, test_initrd)],
529       ["-H", "no_%s" % (constants.HV_INITRD_PATH, )],
530       ["-H", "%s=%s" % (constants.HV_INITRD_PATH, constants.VALUE_DEFAULT)],
531       ])
532   elif default_hv == constants.HT_XEN_HVM:
533     args.extend([
534       ["-H", "%s=acn" % constants.HV_BOOT_ORDER],
535       ["-H", "%s=%s" % (constants.HV_BOOT_ORDER, constants.VALUE_DEFAULT)],
536       ])
537   elif default_hv == constants.HT_KVM and \
538     qa_config.TestEnabled("instance-device-hotplug"):
539     args.extend([
540       ["--net", "-1:add", "--hotplug"],
541       ["--net", "-1:modify,mac=aa:bb:cc:dd:ee:ff", "--hotplug"],
542       ["--net", "-1:remove", "--hotplug"],
543       ["--disk", "-1:add,size=1G", "--hotplug"],
544       ["--disk", "-1:remove", "--hotplug"],
545       ])
546
547   for alist in args:
548     AssertCommand(["gnt-instance", "modify"] + alist + [instance.name])
549
550   # check no-modify
551   AssertCommand(["gnt-instance", "modify", instance.name], fail=True)
552
553   # Marking offline while instance is running must fail...
554   AssertCommand(["gnt-instance", "modify", "--offline", instance.name],
555                  fail=True)
556
557   # ...while making it online is ok, and should work
558   AssertCommand(["gnt-instance", "modify", "--online", instance.name])
559
560
561 @InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
562 def TestInstanceModifyPrimaryAndBack(instance, currentnode, othernode):
563   """gnt-instance modify --new-primary
564
565   This will leave the instance on its original primary node, not other node.
566
567   """
568   if instance.disk_template != constants.DT_FILE:
569     print qa_utils.FormatInfo("Test only supported for the file disk template")
570     return
571
572   cluster_name = qa_config.get("name")
573
574   name = instance.name
575   current = currentnode.primary
576   other = othernode.primary
577
578   filestorage = qa_config.get("file-storage-dir",
579                               pathutils.DEFAULT_FILE_STORAGE_DIR)
580   disk = os.path.join(filestorage, name)
581
582   AssertCommand(["gnt-instance", "modify", "--new-primary=%s" % other, name],
583                 fail=True)
584   AssertCommand(["gnt-instance", "shutdown", name])
585   AssertCommand(["scp", "-oGlobalKnownHostsFile=%s" %
586                  pathutils.SSH_KNOWN_HOSTS_FILE,
587                  "-oCheckHostIp=no", "-oStrictHostKeyChecking=yes",
588                  "-oHashKnownHosts=no", "-oHostKeyAlias=%s" % cluster_name,
589                  "-r", disk, "%s:%s" % (other, filestorage)], node=current)
590   AssertCommand(["gnt-instance", "modify", "--new-primary=%s" % other, name])
591   AssertCommand(["gnt-instance", "startup", name])
592
593   # and back
594   AssertCommand(["gnt-instance", "shutdown", name])
595   AssertCommand(["rm", "-rf", disk], node=other)
596   AssertCommand(["gnt-instance", "modify", "--new-primary=%s" % current, name])
597   AssertCommand(["gnt-instance", "startup", name])
598
599
600 @InstanceCheck(INST_DOWN, INST_DOWN, FIRST_ARG)
601 def TestInstanceStoppedModify(instance):
602   """gnt-instance modify (stopped instance)"""
603   name = instance.name
604
605   # Instance was not marked offline; try marking it online once more
606   AssertCommand(["gnt-instance", "modify", "--online", name])
607
608   # Mark instance as offline
609   AssertCommand(["gnt-instance", "modify", "--offline", name])
610
611   # When the instance is offline shutdown should only work with --force,
612   # while start should never work
613   AssertCommand(["gnt-instance", "shutdown", name], fail=True)
614   AssertCommand(["gnt-instance", "shutdown", "--force", name])
615   AssertCommand(["gnt-instance", "start", name], fail=True)
616   AssertCommand(["gnt-instance", "start", "--force", name], fail=True)
617
618   # Also do offline to offline
619   AssertCommand(["gnt-instance", "modify", "--offline", name])
620
621   # And online again
622   AssertCommand(["gnt-instance", "modify", "--online", name])
623
624
625 @InstanceCheck(INST_DOWN, INST_DOWN, FIRST_ARG)
626 def TestInstanceConvertDiskToPlain(instance, inodes):
627   """gnt-instance modify -t"""
628   name = instance.name
629
630   template = instance.disk_template
631   if template != constants.DT_DRBD8:
632     print qa_utils.FormatInfo("Unsupported template %s, skipping conversion"
633                               " test" % template)
634     return
635
636   assert len(inodes) == 2
637   AssertCommand(["gnt-instance", "modify", "-t", constants.DT_PLAIN, name])
638   AssertCommand(["gnt-instance", "modify", "-t", constants.DT_DRBD8,
639                  "-n", inodes[1].primary, name])
640
641
642 @InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
643 def TestInstanceModifyDisks(instance):
644   """gnt-instance modify --disk"""
645   if not IsDiskSupported(instance):
646     print qa_utils.FormatInfo("Instance doesn't support disks, skipping test")
647     return
648
649   disk_conf = qa_config.GetDiskOptions()[-1]
650   size = disk_conf.get("size")
651   name = instance.name
652   build_cmd = lambda arg: ["gnt-instance", "modify", "--disk", arg, name]
653   if qa_config.AreSpindlesSupported():
654     spindles = disk_conf.get("spindles")
655     spindles_supported = True
656   else:
657     # Any number is good for spindles in this case
658     spindles = 1
659     spindles_supported = False
660   AssertCommand(build_cmd("add:size=%s,spindles=%s" % (size, spindles)),
661                 fail=not spindles_supported)
662   AssertCommand(build_cmd("add:size=%s" % size),
663                 fail=spindles_supported)
664   # Exactly one of the above commands has succeded, so we need one remove
665   AssertCommand(build_cmd("remove"))
666
667
668 @InstanceCheck(INST_DOWN, INST_DOWN, FIRST_ARG)
669 def TestInstanceGrowDisk(instance):
670   """gnt-instance grow-disk"""
671   if instance.disk_template == constants.DT_DISKLESS:
672     print qa_utils.FormatInfo("Test not supported for diskless instances")
673     return
674
675   name = instance.name
676   disks = qa_config.GetDiskOptions()
677   all_size = [d.get("size") for d in disks]
678   all_grow = [d.get("growth") for d in disks]
679
680   if not all_grow:
681     # missing disk sizes but instance grow disk has been enabled,
682     # let's set fixed/nomimal growth
683     all_grow = ["128M" for _ in all_size]
684
685   for idx, (size, grow) in enumerate(zip(all_size, all_grow)):
686     # succeed in grow by amount
687     AssertCommand(["gnt-instance", "grow-disk", name, str(idx), grow])
688     # fail in grow to the old size
689     AssertCommand(["gnt-instance", "grow-disk", "--absolute", name, str(idx),
690                    size], fail=True)
691     # succeed to grow to old size + 2 * growth
692     int_size = utils.ParseUnit(size)
693     int_grow = utils.ParseUnit(grow)
694     AssertCommand(["gnt-instance", "grow-disk", "--absolute", name, str(idx),
695                    str(int_size + 2 * int_grow)])
696
697
698 @InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
699 def TestInstanceDeviceNames(instance):
700   if instance.disk_template == constants.DT_DISKLESS:
701     print qa_utils.FormatInfo("Test not supported for diskless instances")
702     return
703
704   name = instance.name
705   for dev_type in ["disk", "net"]:
706     if dev_type == "disk":
707       options = ",size=512M"
708       if qa_config.AreSpindlesSupported():
709         options += ",spindles=1"
710     else:
711       options = ""
712     # succeed in adding a device named 'test_device'
713     AssertCommand(["gnt-instance", "modify",
714                    "--%s=-1:add,name=test_device%s" % (dev_type, options),
715                    name])
716     # succeed in removing the 'test_device'
717     AssertCommand(["gnt-instance", "modify",
718                    "--%s=test_device:remove" % dev_type,
719                    name])
720     # fail to add two devices with the same name
721     AssertCommand(["gnt-instance", "modify",
722                    "--%s=-1:add,name=test_device%s" % (dev_type, options),
723                    "--%s=-1:add,name=test_device%s" % (dev_type, options),
724                    name], fail=True)
725     # fail to add a device with invalid name
726     AssertCommand(["gnt-instance", "modify",
727                    "--%s=-1:add,name=2%s" % (dev_type, options),
728                    name], fail=True)
729   # Rename disks
730   disks = qa_config.GetDiskOptions()
731   disk_names = [d.get("name") for d in disks]
732   for idx, disk_name in enumerate(disk_names):
733     # Refer to disk by idx
734     AssertCommand(["gnt-instance", "modify",
735                    "--disk=%s:modify,name=renamed" % idx,
736                    name])
737     # Refer to by name and rename to original name
738     AssertCommand(["gnt-instance", "modify",
739                    "--disk=renamed:modify,name=%s" % disk_name,
740                    name])
741   if len(disks) >= 2:
742     # fail in renaming to disks to the same name
743     AssertCommand(["gnt-instance", "modify",
744                    "--disk=0:modify,name=same_name",
745                    "--disk=1:modify,name=same_name",
746                    name], fail=True)
747
748
749 def TestInstanceList():
750   """gnt-instance list"""
751   qa_utils.GenericQueryTest("gnt-instance", query.INSTANCE_FIELDS.keys())
752
753
754 def TestInstanceListFields():
755   """gnt-instance list-fields"""
756   qa_utils.GenericQueryFieldsTest("gnt-instance", query.INSTANCE_FIELDS.keys())
757
758
759 @InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
760 def TestInstanceConsole(instance):
761   """gnt-instance console"""
762   AssertCommand(["gnt-instance", "console", "--show-cmd", instance.name])
763
764
765 @InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
766 def TestReplaceDisks(instance, curr_nodes, other_nodes):
767   """gnt-instance replace-disks"""
768   def buildcmd(args):
769     cmd = ["gnt-instance", "replace-disks"]
770     cmd.extend(args)
771     cmd.append(instance.name)
772     return cmd
773
774   if not IsDiskReplacingSupported(instance):
775     print qa_utils.FormatInfo("Instance doesn't support disk replacing,"
776                               " skipping test")
777     return
778
779   # Currently all supported templates have one primary and one secondary node
780   assert len(curr_nodes) == 2
781   snode = curr_nodes[1]
782   assert len(other_nodes) == 1
783   othernode = other_nodes[0]
784
785   options = qa_config.get("options", {})
786   use_ialloc = options.get("use-iallocators", True)
787   for data in [
788     ["-p"],
789     ["-s"],
790     # A placeholder; the actual command choice depends on use_ialloc
791     None,
792     # Restore the original secondary
793     ["--new-secondary=%s" % snode.primary],
794     ]:
795     if data is None:
796       if use_ialloc:
797         data = ["-I", constants.DEFAULT_IALLOCATOR_SHORTCUT]
798       else:
799         data = ["--new-secondary=%s" % othernode.primary]
800     AssertCommand(buildcmd(data))
801
802   AssertCommand(buildcmd(["-a"]))
803   AssertCommand(["gnt-instance", "stop", instance.name])
804   AssertCommand(buildcmd(["-a"]), fail=True)
805   AssertCommand(["gnt-instance", "activate-disks", instance.name])
806   AssertCommand(["gnt-instance", "activate-disks", "--wait-for-sync",
807                  instance.name])
808   AssertCommand(buildcmd(["-a"]))
809   AssertCommand(["gnt-instance", "start", instance.name])
810
811
812 def _AssertRecreateDisks(cmdargs, instance, fail=False, check=True,
813                          destroy=True):
814   """Execute gnt-instance recreate-disks and check the result
815
816   @param cmdargs: Arguments (instance name excluded)
817   @param instance: Instance to operate on
818   @param fail: True if the command is expected to fail
819   @param check: If True and fail is False, check that the disks work
820   @prama destroy: If True, destroy the old disks first
821
822   """
823   if destroy:
824     _DestroyInstanceDisks(instance)
825   AssertCommand((["gnt-instance", "recreate-disks"] + cmdargs +
826                  [instance.name]), fail)
827   if not fail and check:
828     # Quick check that the disks are there
829     AssertCommand(["gnt-instance", "activate-disks", instance.name])
830     AssertCommand(["gnt-instance", "activate-disks", "--wait-for-sync",
831                    instance.name])
832     AssertCommand(["gnt-instance", "deactivate-disks", instance.name])
833
834
835 def _BuildRecreateDisksOpts(en_disks, with_spindles, with_growth,
836                             spindles_supported):
837   if with_spindles:
838     if spindles_supported:
839       if with_growth:
840         build_spindles_opt = (lambda disk:
841                               ",spindles=%s" %
842                               (disk["spindles"] + disk["spindles-growth"]))
843       else:
844         build_spindles_opt = (lambda disk:
845                               ",spindles=%s" % disk["spindles"])
846     else:
847       build_spindles_opt = (lambda _: ",spindles=1")
848   else:
849     build_spindles_opt = (lambda _: "")
850   if with_growth:
851     build_size_opt = (lambda disk:
852                       "size=%s" % (utils.ParseUnit(disk["size"]) +
853                                    utils.ParseUnit(disk["growth"])))
854   else:
855     build_size_opt = (lambda disk: "size=%s" % disk["size"])
856   build_disk_opt = (lambda (idx, disk):
857                     "--disk=%s:%s%s" % (idx, build_size_opt(disk),
858                                         build_spindles_opt(disk)))
859   return map(build_disk_opt, en_disks)
860
861
862 @InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
863 def TestRecreateDisks(instance, inodes, othernodes):
864   """gnt-instance recreate-disks
865
866   @param instance: Instance to work on
867   @param inodes: List of the current nodes of the instance
868   @param othernodes: list/tuple of nodes where to temporarily recreate disks
869
870   """
871   options = qa_config.get("options", {})
872   use_ialloc = options.get("use-iallocators", True)
873   other_seq = ":".join([n.primary for n in othernodes])
874   orig_seq = ":".join([n.primary for n in inodes])
875   # These fail because the instance is running
876   _AssertRecreateDisks(["-n", other_seq], instance, fail=True, destroy=False)
877   if use_ialloc:
878     _AssertRecreateDisks(["-I", "hail"], instance, fail=True, destroy=False)
879   else:
880     _AssertRecreateDisks(["-n", other_seq], instance, fail=True, destroy=False)
881   AssertCommand(["gnt-instance", "stop", instance.name])
882   # Disks exist: this should fail
883   _AssertRecreateDisks([], instance, fail=True, destroy=False)
884   # Unsupported spindles parameters: fail
885   if not qa_config.AreSpindlesSupported():
886     _AssertRecreateDisks(["--disk=0:spindles=2"], instance,
887                          fail=True, destroy=False)
888   # Recreate disks in place
889   _AssertRecreateDisks([], instance)
890   # Move disks away
891   if use_ialloc:
892     _AssertRecreateDisks(["-I", "hail"], instance)
893     # Move disks somewhere else
894     _AssertRecreateDisks(["-I", constants.DEFAULT_IALLOCATOR_SHORTCUT],
895                          instance)
896   else:
897     _AssertRecreateDisks(["-n", other_seq], instance)
898   # Move disks back
899   _AssertRecreateDisks(["-n", orig_seq], instance)
900   # Recreate resized disks
901   # One of the two commands fails because either spindles are given when they
902   # should not or vice versa
903   alldisks = qa_config.GetDiskOptions()
904   spindles_supported = qa_config.AreSpindlesSupported()
905   disk_opts = _BuildRecreateDisksOpts(enumerate(alldisks), True, True,
906                                       spindles_supported)
907   _AssertRecreateDisks(disk_opts, instance, destroy=True,
908                        fail=not spindles_supported)
909   disk_opts = _BuildRecreateDisksOpts(enumerate(alldisks), False, True,
910                                       spindles_supported)
911   _AssertRecreateDisks(disk_opts, instance, destroy=False,
912                        fail=spindles_supported)
913   # Recreate the disks one by one (with the original size)
914   for (idx, disk) in enumerate(alldisks):
915     # Only the first call should destroy all the disk
916     destroy = (idx == 0)
917     # Again, one of the two commands is expected to fail
918     disk_opts = _BuildRecreateDisksOpts([(idx, disk)], True, False,
919                                         spindles_supported)
920     _AssertRecreateDisks(disk_opts, instance, destroy=destroy, check=False,
921                          fail=not spindles_supported)
922     disk_opts = _BuildRecreateDisksOpts([(idx, disk)], False, False,
923                                         spindles_supported)
924     _AssertRecreateDisks(disk_opts, instance, destroy=False, check=False,
925                          fail=spindles_supported)
926   # This and InstanceCheck decoration check that the disks are working
927   AssertCommand(["gnt-instance", "reinstall", "-f", instance.name])
928   AssertCommand(["gnt-instance", "start", instance.name])
929
930
931 @InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
932 def TestInstanceExport(instance, node):
933   """gnt-backup export -n ..."""
934   name = instance.name
935   # Export does not work for file-based templates, thus we skip the test
936   if instance.disk_template in [constants.DT_FILE, constants.DT_SHARED_FILE]:
937     return
938   AssertCommand(["gnt-backup", "export", "-n", node.primary, name])
939   return qa_utils.ResolveInstanceName(name)
940
941
942 @InstanceCheck(None, INST_DOWN, FIRST_ARG)
943 def TestInstanceExportWithRemove(instance, node):
944   """gnt-backup export --remove-instance"""
945   AssertCommand(["gnt-backup", "export", "-n", node.primary,
946                  "--remove-instance", instance.name])
947
948
949 @InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
950 def TestInstanceExportNoTarget(instance):
951   """gnt-backup export (without target node, should fail)"""
952   AssertCommand(["gnt-backup", "export", instance.name], fail=True)
953
954
955 @InstanceCheck(None, INST_DOWN, FIRST_ARG)
956 def TestInstanceImport(newinst, node, expnode, name):
957   """gnt-backup import"""
958   templ = constants.DT_PLAIN
959   if not qa_config.IsTemplateSupported(templ):
960     return
961   cmd = (["gnt-backup", "import",
962           "--disk-template=%s" % templ,
963           "--no-ip-check",
964           "--src-node=%s" % expnode.primary,
965           "--src-dir=%s/%s" % (pathutils.EXPORT_DIR, name),
966           "--node=%s" % node.primary] +
967          GetGenericAddParameters(newinst, templ,
968                                   force_mac=constants.VALUE_GENERATE))
969   cmd.append(newinst.name)
970   AssertCommand(cmd)
971   newinst.SetDiskTemplate(templ)
972
973
974 def TestBackupList(expnode):
975   """gnt-backup list"""
976   AssertCommand(["gnt-backup", "list", "--node=%s" % expnode.primary])
977
978   qa_utils.GenericQueryTest("gnt-backup", query.EXPORT_FIELDS.keys(),
979                             namefield=None, test_unknown=False)
980
981
982 def TestBackupListFields():
983   """gnt-backup list-fields"""
984   qa_utils.GenericQueryFieldsTest("gnt-backup", query.EXPORT_FIELDS.keys())
985
986
987 def TestRemoveInstanceOfflineNode(instance, snode, set_offline, set_online):
988   """gnt-instance remove with an off-line node
989
990   @param instance: instance
991   @param snode: secondary node, to be set offline
992   @param set_offline: function to call to set the node off-line
993   @param set_online: function to call to set the node on-line
994
995   """
996   info = GetInstanceInfo(instance.name)
997   set_offline(snode)
998   try:
999     TestInstanceRemove(instance)
1000   finally:
1001     set_online(snode)
1002
1003   # Clean up the disks on the offline node, if necessary
1004   if instance.disk_template not in constants.DTS_EXT_MIRROR:
1005     # FIXME: abstract the cleanup inside the disks
1006     if info["storage-type"] == constants.ST_LVM_VG:
1007       for minor in info["drbd-minors"][snode.primary]:
1008         # DRBD 8.3 syntax comes first, then DRBD 8.4 syntax. The 8.4 syntax
1009         # relies on the fact that we always create a resources for each minor,
1010         # and that this resources is always named resource{minor}.
1011         # As 'drbdsetup 0 down' does return success (even though that's invalid
1012         # syntax), we always have to perform both commands and ignore the
1013         # output.
1014         drbd_shutdown_cmd = \
1015           "(drbdsetup %d down >/dev/null 2>&1;" \
1016           " drbdsetup down resource%d >/dev/null 2>&1) || /bin/true" % \
1017             (minor, minor)
1018         AssertCommand(drbd_shutdown_cmd, node=snode)
1019       AssertCommand(["lvremove", "-f"] + info["volumes"], node=snode)
1020     elif info["storage-type"] == constants.ST_FILE:
1021       filestorage = qa_config.get("file-storage-dir",
1022                                   pathutils.DEFAULT_FILE_STORAGE_DIR)
1023       disk = os.path.join(filestorage, instance.name)
1024       AssertCommand(["rm", "-rf", disk], node=snode)
1025
1026
1027 def TestInstanceCreationRestrictedByDiskTemplates():
1028   """Test adding instances for disabled disk templates."""
1029   if qa_config.TestEnabled("cluster-exclusive-storage"):
1030     # These tests are valid only for non-exclusive storage
1031     return
1032
1033   enabled_disk_templates = qa_config.GetEnabledDiskTemplates()
1034   nodes = qa_config.AcquireManyNodes(2)
1035
1036   # Setup the cluster with the enabled_disk_templates
1037   AssertCommand(
1038     ["gnt-cluster", "modify",
1039      "--enabled-disk-templates=%s" % ",".join(enabled_disk_templates),
1040      "--ipolicy-disk-templates=%s" % ",".join(enabled_disk_templates)],
1041     fail=False)
1042
1043   # Test instance creation for enabled disk templates
1044   for disk_template in enabled_disk_templates:
1045     instance = CreateInstanceByDiskTemplate(nodes, disk_template, fail=False)
1046     TestInstanceRemove(instance)
1047     instance.Release()
1048
1049   # Test that instance creation fails for disabled disk templates
1050   disabled_disk_templates = list(constants.DISK_TEMPLATES
1051                                  - set(enabled_disk_templates))
1052   for disk_template in disabled_disk_templates:
1053     instance = CreateInstanceByDiskTemplate(nodes, disk_template, fail=True)
1054
1055   # Test instance creation for after disabling enabled disk templates
1056   if (len(enabled_disk_templates) > 1):
1057     # Partition the disk templates, enable them separately and check if the
1058     # disabled ones cannot be used by instances.
1059     middle = len(enabled_disk_templates) / 2
1060     templates1 = enabled_disk_templates[:middle]
1061     templates2 = enabled_disk_templates[middle:]
1062
1063     for (enabled, disabled) in [(templates1, templates2),
1064                                 (templates2, templates1)]:
1065       AssertCommand(["gnt-cluster", "modify",
1066                      "--enabled-disk-templates=%s" % ",".join(enabled),
1067                      "--ipolicy-disk-templates=%s" % ",".join(enabled)],
1068                     fail=False)
1069       for disk_template in disabled:
1070         CreateInstanceByDiskTemplate(nodes, disk_template, fail=True)
1071   elif (len(enabled_disk_templates) == 1):
1072     # If only one disk template is enabled in the QA config, we have to enable
1073     # some other templates in order to test if the disabling the only enabled
1074     # disk template prohibits creating instances of that template.
1075     other_disk_templates = list(
1076                              set([constants.DT_DISKLESS, constants.DT_BLOCK]) -
1077                              set(enabled_disk_templates))
1078     AssertCommand(["gnt-cluster", "modify",
1079                    "--enabled-disk-templates=%s" %
1080                      ",".join(other_disk_templates),
1081                    "--ipolicy-disk-templates=%s" %
1082                      ",".join(other_disk_templates)],
1083                   fail=False)
1084     CreateInstanceByDiskTemplate(nodes, enabled_disk_templates[0], fail=True)
1085   else:
1086     raise qa_error.Error("Please enable at least one disk template"
1087                          " in your QA setup.")
1088
1089   # Restore initially enabled disk templates
1090   AssertCommand(["gnt-cluster", "modify",
1091                  "--enabled-disk-templates=%s" %
1092                    ",".join(enabled_disk_templates),
1093                  "--ipolicy-disk-templates=%s" %
1094                    ",".join(enabled_disk_templates)],
1095                  fail=False)