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