QA: adjust tests wrt to ipolicy disk templates
[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.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     # 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
538   for alist in args:
539     AssertCommand(["gnt-instance", "modify"] + alist + [instance.name])
540
541   # check no-modify
542   AssertCommand(["gnt-instance", "modify", instance.name], fail=True)
543
544   # Marking offline while instance is running must fail...
545   AssertCommand(["gnt-instance", "modify", "--offline", instance.name],
546                  fail=True)
547
548   # ...while making it online is ok, and should work
549   AssertCommand(["gnt-instance", "modify", "--online", instance.name])
550
551
552 @InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
553 def TestInstanceModifyPrimaryAndBack(instance, currentnode, othernode):
554   """gnt-instance modify --new-primary
555
556   This will leave the instance on its original primary node, not other node.
557
558   """
559   if instance.disk_template != constants.DT_FILE:
560     print qa_utils.FormatInfo("Test only supported for the file disk template")
561     return
562
563   cluster_name = qa_config.get("name")
564
565   name = instance.name
566   current = currentnode.primary
567   other = othernode.primary
568
569   filestorage = qa_config.get("file-storage-dir",
570                               pathutils.DEFAULT_FILE_STORAGE_DIR)
571   disk = os.path.join(filestorage, name)
572
573   AssertCommand(["gnt-instance", "modify", "--new-primary=%s" % other, name],
574                 fail=True)
575   AssertCommand(["gnt-instance", "shutdown", name])
576   AssertCommand(["scp", "-oGlobalKnownHostsFile=%s" %
577                  pathutils.SSH_KNOWN_HOSTS_FILE,
578                  "-oCheckHostIp=no", "-oStrictHostKeyChecking=yes",
579                  "-oHashKnownHosts=no", "-oHostKeyAlias=%s" % cluster_name,
580                  "-r", disk, "%s:%s" % (other, filestorage)], node=current)
581   AssertCommand(["gnt-instance", "modify", "--new-primary=%s" % other, name])
582   AssertCommand(["gnt-instance", "startup", name])
583
584   # and back
585   AssertCommand(["gnt-instance", "shutdown", name])
586   AssertCommand(["rm", "-rf", disk], node=other)
587   AssertCommand(["gnt-instance", "modify", "--new-primary=%s" % current, name])
588   AssertCommand(["gnt-instance", "startup", name])
589
590
591 @InstanceCheck(INST_DOWN, INST_DOWN, FIRST_ARG)
592 def TestInstanceStoppedModify(instance):
593   """gnt-instance modify (stopped instance)"""
594   name = instance.name
595
596   # Instance was not marked offline; try marking it online once more
597   AssertCommand(["gnt-instance", "modify", "--online", name])
598
599   # Mark instance as offline
600   AssertCommand(["gnt-instance", "modify", "--offline", name])
601
602   # When the instance is offline shutdown should only work with --force,
603   # while start should never work
604   AssertCommand(["gnt-instance", "shutdown", name], fail=True)
605   AssertCommand(["gnt-instance", "shutdown", "--force", name])
606   AssertCommand(["gnt-instance", "start", name], fail=True)
607   AssertCommand(["gnt-instance", "start", "--force", name], fail=True)
608
609   # Also do offline to offline
610   AssertCommand(["gnt-instance", "modify", "--offline", name])
611
612   # And online again
613   AssertCommand(["gnt-instance", "modify", "--online", name])
614
615
616 @InstanceCheck(INST_DOWN, INST_DOWN, FIRST_ARG)
617 def TestInstanceConvertDiskToPlain(instance, inodes):
618   """gnt-instance modify -t"""
619   name = instance.name
620
621   template = instance.disk_template
622   if template != constants.DT_DRBD8:
623     print qa_utils.FormatInfo("Unsupported template %s, skipping conversion"
624                               " test" % template)
625     return
626
627   assert len(inodes) == 2
628   AssertCommand(["gnt-instance", "modify", "-t", constants.DT_PLAIN, name])
629   AssertCommand(["gnt-instance", "modify", "-t", constants.DT_DRBD8,
630                  "-n", inodes[1].primary, name])
631
632
633 @InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
634 def TestInstanceModifyDisks(instance):
635   """gnt-instance modify --disk"""
636   if not IsDiskSupported(instance):
637     print qa_utils.FormatInfo("Instance doesn't support disks, skipping test")
638     return
639
640   disk_conf = qa_config.GetDiskOptions()[-1]
641   size = disk_conf.get("size")
642   name = instance.name
643   build_cmd = lambda arg: ["gnt-instance", "modify", "--disk", arg, name]
644   if qa_config.AreSpindlesSupported():
645     spindles = disk_conf.get("spindles")
646     spindles_supported = True
647   else:
648     # Any number is good for spindles in this case
649     spindles = 1
650     spindles_supported = False
651   AssertCommand(build_cmd("add:size=%s,spindles=%s" % (size, spindles)),
652                 fail=not spindles_supported)
653   AssertCommand(build_cmd("add:size=%s" % size),
654                 fail=spindles_supported)
655   # Exactly one of the above commands has succeded, so we need one remove
656   AssertCommand(build_cmd("remove"))
657
658
659 @InstanceCheck(INST_DOWN, INST_DOWN, FIRST_ARG)
660 def TestInstanceGrowDisk(instance):
661   """gnt-instance grow-disk"""
662   if instance.disk_template == constants.DT_DISKLESS:
663     print qa_utils.FormatInfo("Test not supported for diskless instances")
664     return
665
666   name = instance.name
667   disks = qa_config.GetDiskOptions()
668   all_size = [d.get("size") for d in disks]
669   all_grow = [d.get("growth") for d in disks]
670
671   if not all_grow:
672     # missing disk sizes but instance grow disk has been enabled,
673     # let's set fixed/nomimal growth
674     all_grow = ["128M" for _ in all_size]
675
676   for idx, (size, grow) in enumerate(zip(all_size, all_grow)):
677     # succeed in grow by amount
678     AssertCommand(["gnt-instance", "grow-disk", name, str(idx), grow])
679     # fail in grow to the old size
680     AssertCommand(["gnt-instance", "grow-disk", "--absolute", name, str(idx),
681                    size], fail=True)
682     # succeed to grow to old size + 2 * growth
683     int_size = utils.ParseUnit(size)
684     int_grow = utils.ParseUnit(grow)
685     AssertCommand(["gnt-instance", "grow-disk", "--absolute", name, str(idx),
686                    str(int_size + 2 * int_grow)])
687
688
689 @InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
690 def TestInstanceDeviceNames(instance):
691   if instance.disk_template == constants.DT_DISKLESS:
692     print qa_utils.FormatInfo("Test not supported for diskless instances")
693     return
694
695   name = instance.name
696   for dev_type in ["disk", "net"]:
697     if dev_type == "disk":
698       options = ",size=512M"
699       if qa_config.AreSpindlesSupported():
700         options += ",spindles=1"
701     else:
702       options = ""
703     # succeed in adding a device named 'test_device'
704     AssertCommand(["gnt-instance", "modify",
705                    "--%s=-1:add,name=test_device%s" % (dev_type, options),
706                    name])
707     # succeed in removing the 'test_device'
708     AssertCommand(["gnt-instance", "modify",
709                    "--%s=test_device:remove" % dev_type,
710                    name])
711     # fail to add two devices with the same name
712     AssertCommand(["gnt-instance", "modify",
713                    "--%s=-1:add,name=test_device%s" % (dev_type, options),
714                    "--%s=-1:add,name=test_device%s" % (dev_type, options),
715                    name], fail=True)
716     # fail to add a device with invalid name
717     AssertCommand(["gnt-instance", "modify",
718                    "--%s=-1:add,name=2%s" % (dev_type, options),
719                    name], fail=True)
720   # Rename disks
721   disks = qa_config.GetDiskOptions()
722   disk_names = [d.get("name") for d in disks]
723   for idx, disk_name in enumerate(disk_names):
724     # Refer to disk by idx
725     AssertCommand(["gnt-instance", "modify",
726                    "--disk=%s:modify,name=renamed" % idx,
727                    name])
728     # Refer to by name and rename to original name
729     AssertCommand(["gnt-instance", "modify",
730                    "--disk=renamed:modify,name=%s" % disk_name,
731                    name])
732   if len(disks) >= 2:
733     # fail in renaming to disks to the same name
734     AssertCommand(["gnt-instance", "modify",
735                    "--disk=0:modify,name=same_name",
736                    "--disk=1:modify,name=same_name",
737                    name], fail=True)
738
739
740 def TestInstanceList():
741   """gnt-instance list"""
742   qa_utils.GenericQueryTest("gnt-instance", query.INSTANCE_FIELDS.keys())
743
744
745 def TestInstanceListFields():
746   """gnt-instance list-fields"""
747   qa_utils.GenericQueryFieldsTest("gnt-instance", query.INSTANCE_FIELDS.keys())
748
749
750 @InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
751 def TestInstanceConsole(instance):
752   """gnt-instance console"""
753   AssertCommand(["gnt-instance", "console", "--show-cmd", instance.name])
754
755
756 @InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
757 def TestReplaceDisks(instance, curr_nodes, other_nodes):
758   """gnt-instance replace-disks"""
759   def buildcmd(args):
760     cmd = ["gnt-instance", "replace-disks"]
761     cmd.extend(args)
762     cmd.append(instance.name)
763     return cmd
764
765   if not IsDiskReplacingSupported(instance):
766     print qa_utils.FormatInfo("Instance doesn't support disk replacing,"
767                               " skipping test")
768     return
769
770   # Currently all supported templates have one primary and one secondary node
771   assert len(curr_nodes) == 2
772   snode = curr_nodes[1]
773   assert len(other_nodes) == 1
774   othernode = other_nodes[0]
775
776   options = qa_config.get("options", {})
777   use_ialloc = options.get("use-iallocators", True)
778   for data in [
779     ["-p"],
780     ["-s"],
781     # A placeholder; the actual command choice depends on use_ialloc
782     None,
783     # Restore the original secondary
784     ["--new-secondary=%s" % snode.primary],
785     ]:
786     if data is None:
787       if use_ialloc:
788         data = ["-I", constants.DEFAULT_IALLOCATOR_SHORTCUT]
789       else:
790         data = ["--new-secondary=%s" % othernode.primary]
791     AssertCommand(buildcmd(data))
792
793   AssertCommand(buildcmd(["-a"]))
794   AssertCommand(["gnt-instance", "stop", instance.name])
795   AssertCommand(buildcmd(["-a"]), fail=True)
796   AssertCommand(["gnt-instance", "activate-disks", instance.name])
797   AssertCommand(["gnt-instance", "activate-disks", "--wait-for-sync",
798                  instance.name])
799   AssertCommand(buildcmd(["-a"]))
800   AssertCommand(["gnt-instance", "start", instance.name])
801
802
803 def _AssertRecreateDisks(cmdargs, instance, fail=False, check=True,
804                          destroy=True):
805   """Execute gnt-instance recreate-disks and check the result
806
807   @param cmdargs: Arguments (instance name excluded)
808   @param instance: Instance to operate on
809   @param fail: True if the command is expected to fail
810   @param check: If True and fail is False, check that the disks work
811   @prama destroy: If True, destroy the old disks first
812
813   """
814   if destroy:
815     _DestroyInstanceDisks(instance)
816   AssertCommand((["gnt-instance", "recreate-disks"] + cmdargs +
817                  [instance.name]), fail)
818   if not fail and check:
819     # Quick check that the disks are there
820     AssertCommand(["gnt-instance", "activate-disks", instance.name])
821     AssertCommand(["gnt-instance", "activate-disks", "--wait-for-sync",
822                    instance.name])
823     AssertCommand(["gnt-instance", "deactivate-disks", instance.name])
824
825
826 def _BuildRecreateDisksOpts(en_disks, with_spindles, with_growth,
827                             spindles_supported):
828   if with_spindles:
829     if spindles_supported:
830       if with_growth:
831         build_spindles_opt = (lambda disk:
832                               ",spindles=%s" %
833                               (disk["spindles"] + disk["spindles-growth"]))
834       else:
835         build_spindles_opt = (lambda disk:
836                               ",spindles=%s" % disk["spindles"])
837     else:
838       build_spindles_opt = (lambda _: ",spindles=1")
839   else:
840     build_spindles_opt = (lambda _: "")
841   if with_growth:
842     build_size_opt = (lambda disk:
843                       "size=%s" % (utils.ParseUnit(disk["size"]) +
844                                    utils.ParseUnit(disk["growth"])))
845   else:
846     build_size_opt = (lambda disk: "size=%s" % disk["size"])
847   build_disk_opt = (lambda (idx, disk):
848                     "--disk=%s:%s%s" % (idx, build_size_opt(disk),
849                                         build_spindles_opt(disk)))
850   return map(build_disk_opt, en_disks)
851
852
853 @InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
854 def TestRecreateDisks(instance, inodes, othernodes):
855   """gnt-instance recreate-disks
856
857   @param instance: Instance to work on
858   @param inodes: List of the current nodes of the instance
859   @param othernodes: list/tuple of nodes where to temporarily recreate disks
860
861   """
862   options = qa_config.get("options", {})
863   use_ialloc = options.get("use-iallocators", True)
864   other_seq = ":".join([n.primary for n in othernodes])
865   orig_seq = ":".join([n.primary for n in inodes])
866   # These fail because the instance is running
867   _AssertRecreateDisks(["-n", other_seq], instance, fail=True, destroy=False)
868   if use_ialloc:
869     _AssertRecreateDisks(["-I", "hail"], instance, fail=True, destroy=False)
870   else:
871     _AssertRecreateDisks(["-n", other_seq], instance, fail=True, destroy=False)
872   AssertCommand(["gnt-instance", "stop", instance.name])
873   # Disks exist: this should fail
874   _AssertRecreateDisks([], instance, fail=True, destroy=False)
875   # Unsupported spindles parameters: fail
876   if not qa_config.AreSpindlesSupported():
877     _AssertRecreateDisks(["--disk=0:spindles=2"], instance,
878                          fail=True, destroy=False)
879   # Recreate disks in place
880   _AssertRecreateDisks([], instance)
881   # Move disks away
882   if use_ialloc:
883     _AssertRecreateDisks(["-I", "hail"], instance)
884     # Move disks somewhere else
885     _AssertRecreateDisks(["-I", constants.DEFAULT_IALLOCATOR_SHORTCUT],
886                          instance)
887   else:
888     _AssertRecreateDisks(["-n", other_seq], instance)
889   # Move disks back
890   _AssertRecreateDisks(["-n", orig_seq], instance)
891   # Recreate resized disks
892   # One of the two commands fails because either spindles are given when they
893   # should not or vice versa
894   alldisks = qa_config.GetDiskOptions()
895   spindles_supported = qa_config.AreSpindlesSupported()
896   disk_opts = _BuildRecreateDisksOpts(enumerate(alldisks), True, True,
897                                       spindles_supported)
898   _AssertRecreateDisks(disk_opts, instance, destroy=True,
899                        fail=not spindles_supported)
900   disk_opts = _BuildRecreateDisksOpts(enumerate(alldisks), False, True,
901                                       spindles_supported)
902   _AssertRecreateDisks(disk_opts, instance, destroy=False,
903                        fail=spindles_supported)
904   # Recreate the disks one by one (with the original size)
905   for (idx, disk) in enumerate(alldisks):
906     # Only the first call should destroy all the disk
907     destroy = (idx == 0)
908     # Again, one of the two commands is expected to fail
909     disk_opts = _BuildRecreateDisksOpts([(idx, disk)], True, False,
910                                         spindles_supported)
911     _AssertRecreateDisks(disk_opts, instance, destroy=destroy, check=False,
912                          fail=not spindles_supported)
913     disk_opts = _BuildRecreateDisksOpts([(idx, disk)], False, False,
914                                         spindles_supported)
915     _AssertRecreateDisks(disk_opts, instance, destroy=False, check=False,
916                          fail=spindles_supported)
917   # This and InstanceCheck decoration check that the disks are working
918   AssertCommand(["gnt-instance", "reinstall", "-f", instance.name])
919   AssertCommand(["gnt-instance", "start", instance.name])
920
921
922 @InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
923 def TestInstanceExport(instance, node):
924   """gnt-backup export -n ..."""
925   name = instance.name
926   # Export does not work for file-based templates, thus we skip the test
927   if instance.disk_template in [constants.DT_FILE, constants.DT_SHARED_FILE]:
928     return
929   AssertCommand(["gnt-backup", "export", "-n", node.primary, name])
930   return qa_utils.ResolveInstanceName(name)
931
932
933 @InstanceCheck(None, INST_DOWN, FIRST_ARG)
934 def TestInstanceExportWithRemove(instance, node):
935   """gnt-backup export --remove-instance"""
936   AssertCommand(["gnt-backup", "export", "-n", node.primary,
937                  "--remove-instance", instance.name])
938
939
940 @InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
941 def TestInstanceExportNoTarget(instance):
942   """gnt-backup export (without target node, should fail)"""
943   AssertCommand(["gnt-backup", "export", instance.name], fail=True)
944
945
946 @InstanceCheck(None, INST_DOWN, FIRST_ARG)
947 def TestInstanceImport(newinst, node, expnode, name):
948   """gnt-backup import"""
949   templ = constants.DT_PLAIN
950   if not qa_config.IsTemplateSupported(templ):
951     return
952   cmd = (["gnt-backup", "import",
953           "--disk-template=%s" % templ,
954           "--no-ip-check",
955           "--src-node=%s" % expnode.primary,
956           "--src-dir=%s/%s" % (pathutils.EXPORT_DIR, name),
957           "--node=%s" % node.primary] +
958          GetGenericAddParameters(newinst, templ,
959                                   force_mac=constants.VALUE_GENERATE))
960   cmd.append(newinst.name)
961   AssertCommand(cmd)
962   newinst.SetDiskTemplate(templ)
963
964
965 def TestBackupList(expnode):
966   """gnt-backup list"""
967   AssertCommand(["gnt-backup", "list", "--node=%s" % expnode.primary])
968
969   qa_utils.GenericQueryTest("gnt-backup", query.EXPORT_FIELDS.keys(),
970                             namefield=None, test_unknown=False)
971
972
973 def TestBackupListFields():
974   """gnt-backup list-fields"""
975   qa_utils.GenericQueryFieldsTest("gnt-backup", query.EXPORT_FIELDS.keys())
976
977
978 def TestRemoveInstanceOfflineNode(instance, snode, set_offline, set_online):
979   """gnt-instance remove with an off-line node
980
981   @param instance: instance
982   @param snode: secondary node, to be set offline
983   @param set_offline: function to call to set the node off-line
984   @param set_online: function to call to set the node on-line
985
986   """
987   info = GetInstanceInfo(instance.name)
988   set_offline(snode)
989   try:
990     TestInstanceRemove(instance)
991   finally:
992     set_online(snode)
993
994   # Clean up the disks on the offline node, if necessary
995   if instance.disk_template not in constants.DTS_EXT_MIRROR:
996     # FIXME: abstract the cleanup inside the disks
997     if info["storage-type"] == constants.ST_LVM_VG:
998       for minor in info["drbd-minors"][snode.primary]:
999         # DRBD 8.3 syntax comes first, then DRBD 8.4 syntax. The 8.4 syntax
1000         # relies on the fact that we always create a resources for each minor,
1001         # and that this resources is always named resource{minor}.
1002         # As 'drbdsetup 0 down' does return success (even though that's invalid
1003         # syntax), we always have to perform both commands and ignore the
1004         # output.
1005         drbd_shutdown_cmd = \
1006           "(drbdsetup %d down >/dev/null 2>&1;" \
1007           " drbdsetup down resource%d >/dev/null 2>&1) || /bin/true" % \
1008             (minor, minor)
1009         AssertCommand(drbd_shutdown_cmd, node=snode)
1010       AssertCommand(["lvremove", "-f"] + info["volumes"], node=snode)
1011     elif info["storage-type"] == constants.ST_FILE:
1012       filestorage = qa_config.get("file-storage-dir",
1013                                   pathutils.DEFAULT_FILE_STORAGE_DIR)
1014       disk = os.path.join(filestorage, instance.name)
1015       AssertCommand(["rm", "-rf", disk], node=snode)
1016
1017
1018 def TestInstanceCreationRestrictedByDiskTemplates():
1019   """Test adding instances for disabled disk templates."""
1020   if qa_config.TestEnabled("cluster-exclusive-storage"):
1021     # These tests are valid only for non-exclusive storage
1022     return
1023
1024   enabled_disk_templates = qa_config.GetEnabledDiskTemplates()
1025   nodes = qa_config.AcquireManyNodes(2)
1026
1027   # Setup the cluster with the enabled_disk_templates
1028   AssertCommand(
1029     ["gnt-cluster", "modify",
1030      "--enabled-disk-templates=%s" % ",".join(enabled_disk_templates),
1031      "--ipolicy-disk-templates=%s" % ",".join(enabled_disk_templates)],
1032     fail=False)
1033
1034   # Test instance creation for enabled disk templates
1035   for disk_template in enabled_disk_templates:
1036     instance = CreateInstanceByDiskTemplate(nodes, disk_template, fail=False)
1037     TestInstanceRemove(instance)
1038     instance.Release()
1039
1040   # Test that instance creation fails for disabled disk templates
1041   disabled_disk_templates = list(constants.DISK_TEMPLATES
1042                                  - set(enabled_disk_templates))
1043   for disk_template in disabled_disk_templates:
1044     instance = CreateInstanceByDiskTemplate(nodes, disk_template, fail=True)
1045
1046   # Test instance creation for after disabling enabled disk templates
1047   if (len(enabled_disk_templates) > 1):
1048     # Partition the disk templates, enable them separately and check if the
1049     # disabled ones cannot be used by instances.
1050     middle = len(enabled_disk_templates) / 2
1051     templates1 = enabled_disk_templates[:middle]
1052     templates2 = enabled_disk_templates[middle:]
1053
1054     for (enabled, disabled) in [(templates1, templates2),
1055                                 (templates2, templates1)]:
1056       AssertCommand(["gnt-cluster", "modify",
1057                      "--enabled-disk-templates=%s" % ",".join(enabled),
1058                      "--ipolicy-disk-templates=%s" % ",".join(enabled)],
1059                     fail=False)
1060       for disk_template in disabled:
1061         CreateInstanceByDiskTemplate(nodes, disk_template, fail=True)
1062   elif (len(enabled_disk_templates) == 1):
1063     # If only one disk template is enabled in the QA config, we have to enable
1064     # some other templates in order to test if the disabling the only enabled
1065     # disk template prohibits creating instances of that template.
1066     other_disk_templates = list(
1067                              set([constants.DT_DISKLESS, constants.DT_BLOCK]) -
1068                              set(enabled_disk_templates))
1069     AssertCommand(["gnt-cluster", "modify",
1070                    "--enabled-disk-templates=%s" %
1071                      ",".join(other_disk_templates),
1072                    "--ipolicy-disk-templates=%s" %
1073                      ",".join(other_disk_templates)],
1074                   fail=False)
1075     CreateInstanceByDiskTemplate(nodes, enabled_disk_templates[0], fail=True)
1076   else:
1077     raise qa_error.Error("Please enable at least one disk template"
1078                          " in your QA setup.")
1079
1080   # Restore initially enabled disk templates
1081   AssertCommand(["gnt-cluster", "modify",
1082                  "--enabled-disk-templates=%s" %
1083                    ",".join(enabled_disk_templates),
1084                  "--ipolicy-disk-templates=%s" %
1085                    ",".join(enabled_disk_templates)],
1086                  fail=False)