0f3b8771e1d53e7276aa486da116c9adb0e00933
[ganeti-local] / qa / qa_instance.py
1 #
2 #
3
4 # Copyright (C) 2007, 2011, 2012, 2013 Google Inc.
5 #
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful, but
12 # WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14 # General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19 # 02110-1301, USA.
20
21
22 """Instance related QA tests.
23
24 """
25
26 import operator
27 import os
28 import re
29
30 from ganeti import utils
31 from ganeti import constants
32 from ganeti import query
33 from ganeti import pathutils
34
35 import qa_config
36 import qa_utils
37 import qa_error
38
39 from qa_utils import AssertIn, AssertCommand, AssertEqual
40 from qa_utils import InstanceCheck, INST_DOWN, INST_UP, FIRST_ARG, RETURN_VALUE
41
42
43 def _GetDiskStatePath(disk):
44   return "/sys/block/%s/device/state" % disk
45
46
47 def _GetGenericAddParameters(inst, disk_template, force_mac=None):
48   params = ["-B"]
49   params.append("%s=%s,%s=%s" % (constants.BE_MINMEM,
50                                  qa_config.get(constants.BE_MINMEM),
51                                  constants.BE_MAXMEM,
52                                  qa_config.get(constants.BE_MAXMEM)))
53
54   if disk_template != constants.DT_DISKLESS:
55     for idx, size in enumerate(qa_config.get("disk")):
56       params.extend(["--disk", "%s:size=%s" % (idx, size)])
57
58   # Set static MAC address if configured
59   if force_mac:
60     nic0_mac = force_mac
61   else:
62     nic0_mac = inst.GetNicMacAddr(0, None)
63
64   if nic0_mac:
65     params.extend(["--net", "0:mac=%s" % nic0_mac])
66
67   return params
68
69
70 def _DiskTest(node, disk_template, fail=False):
71   instance = qa_config.AcquireInstance()
72   try:
73     cmd = (["gnt-instance", "add",
74             "--os-type=%s" % qa_config.get("os"),
75             "--disk-template=%s" % disk_template,
76             "--node=%s" % node] +
77            _GetGenericAddParameters(instance, disk_template))
78     cmd.append(instance.name)
79
80     AssertCommand(cmd, fail=fail)
81
82     if not fail:
83       _CheckSsconfInstanceList(instance.name)
84       instance.SetDiskTemplate(disk_template)
85
86       return instance
87   except:
88     instance.Release()
89     raise
90
91   # Handle the case where creation is expected to fail
92   assert fail
93   instance.Release()
94   return None
95
96
97 def _GetInstanceInfo(instance):
98   """Return information about the actual state of an instance.
99
100   @type instance: string
101   @param instance: the instance name
102   @return: a dictionary with the following keys:
103       - "nodes": instance nodes, a list of strings
104       - "volumes": instance volume IDs, a list of strings
105       - "drbd-minors": DRBD minors used by the instance, a dictionary where
106         keys are nodes, and values are lists of integers (or an empty
107         dictionary for non-DRBD instances)
108       - "disk-template": instance disk template
109       - "storage-type": storage type associated with the instance disk template
110
111   """
112   node_elem = r"([^,()]+)(?:\s+\([^)]+\))?"
113   # re_nodelist matches a list of nodes returned by gnt-instance info, e.g.:
114   #  node1.fqdn
115   #  node2.fqdn,node3.fqdn
116   #  node4.fqdn (group mygroup, group UUID 01234567-abcd-0123-4567-0123456789ab)
117   # FIXME This works with no more than 2 secondaries
118   re_nodelist = re.compile(node_elem + "(?:," + node_elem + ")?$")
119
120   info = qa_utils.GetObjectInfo(["gnt-instance", "info", instance])[0]
121   nodes = []
122   for nodeinfo in info["Nodes"]:
123     if "primary" in nodeinfo:
124       nodes.append(nodeinfo["primary"])
125     elif "secondaries" in nodeinfo:
126       nodestr = nodeinfo["secondaries"]
127       if nodestr:
128         m = re_nodelist.match(nodestr)
129         if m:
130           nodes.extend(filter(None, m.groups()))
131         else:
132           nodes.append(nodestr)
133
134   disk_template = info["Disk template"]
135   if not disk_template:
136     raise qa_error.Error("Can't get instance disk template")
137   storage_type = constants.DISK_TEMPLATES_STORAGE_TYPE[disk_template]
138
139   re_drbdnode = re.compile(r"^([^\s,]+),\s+minor=([0-9]+)$")
140   vols = []
141   drbd_min = {}
142   for (count, diskinfo) in enumerate(info["Disks"]):
143     (dtype, _) = diskinfo["disk/%s" % count].split(",", 1)
144     if dtype == constants.LD_DRBD8:
145       for child in diskinfo["child devices"]:
146         vols.append(child["logical_id"])
147       for key in ["nodeA", "nodeB"]:
148         m = re_drbdnode.match(diskinfo[key])
149         if not m:
150           raise qa_error.Error("Cannot parse DRBD info: %s" % diskinfo[key])
151         node = m.group(1)
152         minor = int(m.group(2))
153         minorlist = drbd_min.setdefault(node, [])
154         minorlist.append(minor)
155     elif dtype == constants.LD_LV:
156       vols.append(diskinfo["logical_id"])
157
158   assert nodes
159   assert len(nodes) < 2 or vols
160   return {
161     "nodes": nodes,
162     "volumes": vols,
163     "drbd-minors": drbd_min,
164     "disk-template": disk_template,
165     "storage-type": storage_type,
166     }
167
168
169 def _DestroyInstanceVolumes(instance):
170   """Remove all the LVM volumes of an instance.
171
172   This is used to simulate HW errors (dead nodes, broken disks...); the
173   configuration of the instance is not affected.
174   @type instance: dictionary
175   @param instance: the instance
176
177   """
178   info = _GetInstanceInfo(instance.name)
179   vols = info["volumes"]
180   for node in info["nodes"]:
181     AssertCommand(["lvremove", "-f"] + vols, node=node)
182
183
184 def _GetInstanceField(instance, field):
185   """Get the value of a field of an instance.
186
187   @type instance: string
188   @param instance: Instance name
189   @type field: string
190   @param field: Name of the field
191   @rtype: string
192
193   """
194   master = qa_config.GetMasterNode()
195   infocmd = utils.ShellQuoteArgs(["gnt-instance", "list", "--no-headers",
196                                   "--units", "m", "-o", field, instance])
197   return qa_utils.GetCommandOutput(master.primary, infocmd).strip()
198
199
200 def _GetBoolInstanceField(instance, field):
201   """Get the Boolean value of a field of an instance.
202
203   @type instance: string
204   @param instance: Instance name
205   @type field: string
206   @param field: Name of the field
207   @rtype: bool
208
209   """
210   info_out = _GetInstanceField(instance, field)
211   if info_out == "Y":
212     return True
213   elif info_out == "N":
214     return False
215   else:
216     raise qa_error.Error("Field %s of instance %s has a non-Boolean value:"
217                          " %s" % (field, instance, info_out))
218
219
220 def _GetNumInstanceField(instance, field):
221   """Get a numeric value of a field of an instance.
222
223   @type instance: string
224   @param instance: Instance name
225   @type field: string
226   @param field: Name of the field
227   @rtype: int or float
228
229   """
230   info_out = _GetInstanceField(instance, field)
231   try:
232     ret = int(info_out)
233   except ValueError:
234     try:
235       ret = float(info_out)
236     except ValueError:
237       raise qa_error.Error("Field %s of instance %s has a non-numeric value:"
238                            " %s" % (field, instance, info_out))
239   return ret
240
241
242 def GetInstanceSpec(instance, spec):
243   """Return the current spec for the given parameter.
244
245   @type instance: string
246   @param instance: Instance name
247   @type spec: string
248   @param spec: one of the supported parameters: "mem-size", "cpu-count",
249       "disk-count", "disk-size", "nic-count"
250   @rtype: tuple
251   @return: (minspec, maxspec); minspec and maxspec can be different only for
252       memory and disk size
253
254   """
255   specmap = {
256     "mem-size": ["be/minmem", "be/maxmem"],
257     "cpu-count": ["vcpus"],
258     "disk-count": ["disk.count"],
259     "disk-size": ["disk.size/ "],
260     "nic-count": ["nic.count"],
261     }
262   # For disks, first we need the number of disks
263   if spec == "disk-size":
264     (numdisk, _) = GetInstanceSpec(instance, "disk-count")
265     fields = ["disk.size/%s" % k for k in range(0, numdisk)]
266   else:
267     assert spec in specmap, "%s not in %s" % (spec, specmap)
268     fields = specmap[spec]
269   values = [_GetNumInstanceField(instance, f) for f in fields]
270   return (min(values), max(values))
271
272
273 def IsFailoverSupported(instance):
274   return instance.disk_template in constants.DTS_MIRRORED
275
276
277 def IsMigrationSupported(instance):
278   return instance.disk_template in constants.DTS_MIRRORED
279
280
281 def IsDiskReplacingSupported(instance):
282   return instance.disk_template == constants.DT_DRBD8
283
284
285 def TestInstanceAddWithPlainDisk(nodes, fail=False):
286   """gnt-instance add -t plain"""
287   assert len(nodes) == 1
288   instance = _DiskTest(nodes[0].primary, constants.DT_PLAIN, fail=fail)
289   if not fail:
290     qa_utils.RunInstanceCheck(instance, True)
291   return instance
292
293
294 @InstanceCheck(None, INST_UP, RETURN_VALUE)
295 def TestInstanceAddWithDrbdDisk(nodes):
296   """gnt-instance add -t drbd"""
297   assert len(nodes) == 2
298   return _DiskTest(":".join(map(operator.attrgetter("primary"), nodes)),
299                    constants.DT_DRBD8)
300
301
302 @InstanceCheck(None, INST_UP, RETURN_VALUE)
303 def TestInstanceAddFile(nodes):
304   """gnt-instance add -t file"""
305   assert len(nodes) == 1
306   return _DiskTest(nodes[0].primary, constants.DT_FILE)
307
308
309 @InstanceCheck(None, INST_UP, RETURN_VALUE)
310 def TestInstanceAddDiskless(nodes):
311   """gnt-instance add -t diskless"""
312   assert len(nodes) == 1
313   return _DiskTest(nodes[0].primary, constants.DT_DISKLESS)
314
315
316 @InstanceCheck(None, INST_DOWN, FIRST_ARG)
317 def TestInstanceRemove(instance):
318   """gnt-instance remove"""
319   AssertCommand(["gnt-instance", "remove", "-f", instance.name])
320
321
322 @InstanceCheck(INST_DOWN, INST_UP, FIRST_ARG)
323 def TestInstanceStartup(instance):
324   """gnt-instance startup"""
325   AssertCommand(["gnt-instance", "startup", instance.name])
326
327
328 @InstanceCheck(INST_UP, INST_DOWN, FIRST_ARG)
329 def TestInstanceShutdown(instance):
330   """gnt-instance shutdown"""
331   AssertCommand(["gnt-instance", "shutdown", instance.name])
332
333
334 @InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
335 def TestInstanceReboot(instance):
336   """gnt-instance reboot"""
337   options = qa_config.get("options", {})
338   reboot_types = options.get("reboot-types", constants.REBOOT_TYPES)
339   name = instance.name
340   for rtype in reboot_types:
341     AssertCommand(["gnt-instance", "reboot", "--type=%s" % rtype, name])
342
343   AssertCommand(["gnt-instance", "shutdown", name])
344   qa_utils.RunInstanceCheck(instance, False)
345   AssertCommand(["gnt-instance", "reboot", name])
346
347   master = qa_config.GetMasterNode()
348   cmd = ["gnt-instance", "list", "--no-headers", "-o", "status", name]
349   result_output = qa_utils.GetCommandOutput(master.primary,
350                                             utils.ShellQuoteArgs(cmd))
351   AssertEqual(result_output.strip(), constants.INSTST_RUNNING)
352
353
354 @InstanceCheck(INST_DOWN, INST_DOWN, FIRST_ARG)
355 def TestInstanceReinstall(instance):
356   """gnt-instance reinstall"""
357   if instance.disk_template == constants.DT_DISKLESS:
358     print qa_utils.FormatInfo("Test not supported for diskless instances")
359     return
360
361   AssertCommand(["gnt-instance", "reinstall", "-f", instance.name])
362
363   # Test with non-existant OS definition
364   AssertCommand(["gnt-instance", "reinstall", "-f",
365                  "--os-type=NonExistantOsForQa",
366                  instance.name],
367                 fail=True)
368
369
370 def _ReadSsconfInstanceList():
371   """Reads ssconf_instance_list from the master node.
372
373   """
374   master = qa_config.GetMasterNode()
375
376   ssconf_path = utils.PathJoin(pathutils.DATA_DIR,
377                                "ssconf_%s" % constants.SS_INSTANCE_LIST)
378
379   cmd = ["cat", qa_utils.MakeNodePath(master, ssconf_path)]
380
381   return qa_utils.GetCommandOutput(master.primary,
382                                    utils.ShellQuoteArgs(cmd)).splitlines()
383
384
385 def _CheckSsconfInstanceList(instance):
386   """Checks if a certain instance is in the ssconf instance list.
387
388   @type instance: string
389   @param instance: Instance name
390
391   """
392   AssertIn(qa_utils.ResolveInstanceName(instance),
393            _ReadSsconfInstanceList())
394
395
396 @InstanceCheck(INST_DOWN, INST_DOWN, FIRST_ARG)
397 def TestInstanceRenameAndBack(rename_source, rename_target):
398   """gnt-instance rename
399
400   This must leave the instance with the original name, not the target
401   name.
402
403   """
404   _CheckSsconfInstanceList(rename_source)
405
406   # first do a rename to a different actual name, expecting it to fail
407   qa_utils.AddToEtcHosts(["meeeeh-not-exists", rename_target])
408   try:
409     AssertCommand(["gnt-instance", "rename", rename_source, rename_target],
410                   fail=True)
411     _CheckSsconfInstanceList(rename_source)
412   finally:
413     qa_utils.RemoveFromEtcHosts(["meeeeh-not-exists", rename_target])
414
415   info = _GetInstanceInfo(rename_source)
416
417   # Check instance volume tags correctly updated. Note that this check is lvm
418   # specific, so we skip it for non-lvm-based instances.
419   # FIXME: This will need updating when instances will be able to have
420   # different disks living on storage pools with etherogeneous storage types.
421   # FIXME: This check should be put inside the disk/storage class themselves,
422   # rather than explicitly called here.
423   if info["storage-type"] == constants.ST_LVM_VG:
424     # In the lvm world we can check for tags on the logical volume
425     tags_cmd = ("lvs -o tags --noheadings %s | grep " %
426                 (" ".join(info["volumes"]), ))
427   else:
428     # Other storage types don't have tags, so we use an always failing command,
429     # to make sure it never gets executed
430     tags_cmd = "false"
431
432   # and now rename instance to rename_target...
433   AssertCommand(["gnt-instance", "rename", rename_source, rename_target])
434   _CheckSsconfInstanceList(rename_target)
435   qa_utils.RunInstanceCheck(rename_source, False)
436   qa_utils.RunInstanceCheck(rename_target, False)
437
438   # NOTE: tags might not be the exactly as the instance name, due to
439   # charset restrictions; hence the test might be flaky
440   if (rename_source != rename_target and
441       info["storage-type"] == constants.ST_LVM_VG):
442     for node in info["nodes"]:
443       AssertCommand(tags_cmd + rename_source, node=node, fail=True)
444       AssertCommand(tags_cmd + rename_target, node=node, fail=False)
445
446   # and back
447   AssertCommand(["gnt-instance", "rename", rename_target, rename_source])
448   _CheckSsconfInstanceList(rename_source)
449   qa_utils.RunInstanceCheck(rename_target, False)
450
451   if (rename_source != rename_target and
452       info["storage-type"] == constants.ST_LVM_VG):
453     for node in info["nodes"]:
454       AssertCommand(tags_cmd + rename_source, node=node, fail=False)
455       AssertCommand(tags_cmd + rename_target, node=node, fail=True)
456
457
458 @InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
459 def TestInstanceFailover(instance):
460   """gnt-instance failover"""
461   if not IsFailoverSupported(instance):
462     print qa_utils.FormatInfo("Instance doesn't support failover, skipping"
463                               " test")
464     return
465
466   cmd = ["gnt-instance", "failover", "--force", instance.name]
467
468   # failover ...
469   AssertCommand(cmd)
470   qa_utils.RunInstanceCheck(instance, True)
471
472   # ... and back
473   AssertCommand(cmd)
474
475
476 @InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
477 def TestInstanceMigrate(instance, toggle_always_failover=True):
478   """gnt-instance migrate"""
479   if not IsMigrationSupported(instance):
480     print qa_utils.FormatInfo("Instance doesn't support migration, skipping"
481                               " test")
482     return
483
484   cmd = ["gnt-instance", "migrate", "--force", instance.name]
485   af_par = constants.BE_ALWAYS_FAILOVER
486   af_field = "be/" + constants.BE_ALWAYS_FAILOVER
487   af_init_val = _GetBoolInstanceField(instance.name, af_field)
488
489   # migrate ...
490   AssertCommand(cmd)
491   # TODO: Verify the choice between failover and migration
492   qa_utils.RunInstanceCheck(instance, True)
493
494   # ... and back (possibly with always_failover toggled)
495   if toggle_always_failover:
496     AssertCommand(["gnt-instance", "modify", "-B",
497                    ("%s=%s" % (af_par, not af_init_val)),
498                    instance.name])
499   AssertCommand(cmd)
500   # TODO: Verify the choice between failover and migration
501   qa_utils.RunInstanceCheck(instance, True)
502   if toggle_always_failover:
503     AssertCommand(["gnt-instance", "modify", "-B",
504                    ("%s=%s" % (af_par, af_init_val)), instance.name])
505
506   # TODO: Split into multiple tests
507   AssertCommand(["gnt-instance", "shutdown", instance.name])
508   qa_utils.RunInstanceCheck(instance, False)
509   AssertCommand(cmd, fail=True)
510   AssertCommand(["gnt-instance", "migrate", "--force", "--allow-failover",
511                  instance.name])
512   AssertCommand(["gnt-instance", "start", instance.name])
513   AssertCommand(cmd)
514   # @InstanceCheck enforces the check that the instance is running
515   qa_utils.RunInstanceCheck(instance, True)
516
517   AssertCommand(["gnt-instance", "modify", "-B",
518                  ("%s=%s" %
519                   (constants.BE_ALWAYS_FAILOVER, constants.VALUE_TRUE)),
520                  instance.name])
521
522   AssertCommand(cmd)
523   qa_utils.RunInstanceCheck(instance, True)
524   # TODO: Verify that a failover has been done instead of a migration
525
526   # TODO: Verify whether the default value is restored here (not hardcoded)
527   AssertCommand(["gnt-instance", "modify", "-B",
528                  ("%s=%s" %
529                   (constants.BE_ALWAYS_FAILOVER, constants.VALUE_FALSE)),
530                  instance.name])
531
532   AssertCommand(cmd)
533   qa_utils.RunInstanceCheck(instance, True)
534
535
536 def TestInstanceInfo(instance):
537   """gnt-instance info"""
538   AssertCommand(["gnt-instance", "info", instance.name])
539
540
541 @InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
542 def TestInstanceModify(instance):
543   """gnt-instance modify"""
544   default_hv = qa_config.GetDefaultHypervisor()
545
546   # Assume /sbin/init exists on all systems
547   test_kernel = "/sbin/init"
548   test_initrd = test_kernel
549
550   orig_maxmem = qa_config.get(constants.BE_MAXMEM)
551   orig_minmem = qa_config.get(constants.BE_MINMEM)
552   #orig_bridge = qa_config.get("bridge", "xen-br0")
553
554   args = [
555     ["-B", "%s=128" % constants.BE_MINMEM],
556     ["-B", "%s=128" % constants.BE_MAXMEM],
557     ["-B", "%s=%s,%s=%s" % (constants.BE_MINMEM, orig_minmem,
558                             constants.BE_MAXMEM, orig_maxmem)],
559     ["-B", "%s=2" % constants.BE_VCPUS],
560     ["-B", "%s=1" % constants.BE_VCPUS],
561     ["-B", "%s=%s" % (constants.BE_VCPUS, constants.VALUE_DEFAULT)],
562     ["-B", "%s=%s" % (constants.BE_ALWAYS_FAILOVER, constants.VALUE_TRUE)],
563     ["-B", "%s=%s" % (constants.BE_ALWAYS_FAILOVER, constants.VALUE_DEFAULT)],
564
565     ["-H", "%s=%s" % (constants.HV_KERNEL_PATH, test_kernel)],
566     ["-H", "%s=%s" % (constants.HV_KERNEL_PATH, constants.VALUE_DEFAULT)],
567
568     # TODO: bridge tests
569     #["--bridge", "xen-br1"],
570     #["--bridge", orig_bridge],
571     ]
572
573   if default_hv == constants.HT_XEN_PVM:
574     args.extend([
575       ["-H", "%s=%s" % (constants.HV_INITRD_PATH, test_initrd)],
576       ["-H", "no_%s" % (constants.HV_INITRD_PATH, )],
577       ["-H", "%s=%s" % (constants.HV_INITRD_PATH, constants.VALUE_DEFAULT)],
578       ])
579   elif default_hv == constants.HT_XEN_HVM:
580     args.extend([
581       ["-H", "%s=acn" % constants.HV_BOOT_ORDER],
582       ["-H", "%s=%s" % (constants.HV_BOOT_ORDER, constants.VALUE_DEFAULT)],
583       ])
584
585   for alist in args:
586     AssertCommand(["gnt-instance", "modify"] + alist + [instance.name])
587
588   # check no-modify
589   AssertCommand(["gnt-instance", "modify", instance.name], fail=True)
590
591   # Marking offline while instance is running must fail...
592   AssertCommand(["gnt-instance", "modify", "--offline", instance.name],
593                  fail=True)
594
595   # ...while making it online is ok, and should work
596   AssertCommand(["gnt-instance", "modify", "--online", instance.name])
597
598
599 @InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
600 def TestInstanceModifyPrimaryAndBack(instance, currentnode, othernode):
601   """gnt-instance modify --new-primary
602
603   This will leave the instance on its original primary node, not other node.
604
605   """
606   if instance.disk_template != constants.DT_FILE:
607     print qa_utils.FormatInfo("Test only supported for the file disk template")
608     return
609
610   cluster_name = qa_config.get("name")
611
612   name = instance.name
613   current = currentnode.primary
614   other = othernode.primary
615
616   # FIXME: the qa doesn't have a customizable file storage dir parameter. As
617   # such for now we use the default.
618   filestorage = pathutils.DEFAULT_FILE_STORAGE_DIR
619   disk = os.path.join(filestorage, name)
620
621   AssertCommand(["gnt-instance", "modify", "--new-primary=%s" % other, name],
622                 fail=True)
623   AssertCommand(["gnt-instance", "shutdown", name])
624   AssertCommand(["scp", "-oGlobalKnownHostsFile=%s" %
625                  pathutils.SSH_KNOWN_HOSTS_FILE,
626                  "-oCheckHostIp=no", "-oStrictHostKeyChecking=yes",
627                  "-oHashKnownHosts=no", "-oHostKeyAlias=%s" % cluster_name,
628                  "-r", disk, "%s:%s" % (other, filestorage)])
629   AssertCommand(["gnt-instance", "modify", "--new-primary=%s" % other, name])
630   AssertCommand(["gnt-instance", "startup", name])
631
632   # and back
633   AssertCommand(["gnt-instance", "shutdown", name])
634   AssertCommand(["rm", "-rf", disk], node=other)
635   AssertCommand(["gnt-instance", "modify", "--new-primary=%s" % current, name])
636   AssertCommand(["gnt-instance", "startup", name])
637
638
639 @InstanceCheck(INST_DOWN, INST_DOWN, FIRST_ARG)
640 def TestInstanceStoppedModify(instance):
641   """gnt-instance modify (stopped instance)"""
642   name = instance.name
643
644   # Instance was not marked offline; try marking it online once more
645   AssertCommand(["gnt-instance", "modify", "--online", name])
646
647   # Mark instance as offline
648   AssertCommand(["gnt-instance", "modify", "--offline", name])
649
650   # When the instance is offline shutdown should only work with --force,
651   # while start should never work
652   AssertCommand(["gnt-instance", "shutdown", name], fail=True)
653   AssertCommand(["gnt-instance", "shutdown", "--force", name])
654   AssertCommand(["gnt-instance", "start", name], fail=True)
655   AssertCommand(["gnt-instance", "start", "--force", name], fail=True)
656
657   # Also do offline to offline
658   AssertCommand(["gnt-instance", "modify", "--offline", name])
659
660   # And online again
661   AssertCommand(["gnt-instance", "modify", "--online", name])
662
663
664 @InstanceCheck(INST_DOWN, INST_DOWN, FIRST_ARG)
665 def TestInstanceConvertDiskToPlain(instance, inodes):
666   """gnt-instance modify -t"""
667   name = instance.name
668
669   template = instance.disk_template
670   if template != constants.DT_DRBD8:
671     print qa_utils.FormatInfo("Unsupported template %s, skipping conversion"
672                               " test" % template)
673     return
674
675   assert len(inodes) == 2
676   AssertCommand(["gnt-instance", "modify", "-t", constants.DT_PLAIN, name])
677   AssertCommand(["gnt-instance", "modify", "-t", constants.DT_DRBD8,
678                  "-n", inodes[1].primary, name])
679
680
681 @InstanceCheck(INST_DOWN, INST_DOWN, FIRST_ARG)
682 def TestInstanceGrowDisk(instance):
683   """gnt-instance grow-disk"""
684   if qa_config.GetExclusiveStorage():
685     print qa_utils.FormatInfo("Test not supported with exclusive_storage")
686     return
687
688   if instance.disk_template == constants.DT_DISKLESS:
689     print qa_utils.FormatInfo("Test not supported for diskless instances")
690     return
691
692   name = instance.name
693   all_size = qa_config.get("disk")
694   all_grow = qa_config.get("disk-growth")
695
696   if not all_grow:
697     # missing disk sizes but instance grow disk has been enabled,
698     # let's set fixed/nomimal growth
699     all_grow = ["128M" for _ in all_size]
700
701   for idx, (size, grow) in enumerate(zip(all_size, all_grow)):
702     # succeed in grow by amount
703     AssertCommand(["gnt-instance", "grow-disk", name, str(idx), grow])
704     # fail in grow to the old size
705     AssertCommand(["gnt-instance", "grow-disk", "--absolute", name, str(idx),
706                    size], fail=True)
707     # succeed to grow to old size + 2 * growth
708     int_size = utils.ParseUnit(size)
709     int_grow = utils.ParseUnit(grow)
710     AssertCommand(["gnt-instance", "grow-disk", "--absolute", name, str(idx),
711                    str(int_size + 2 * int_grow)])
712
713
714 def TestInstanceList():
715   """gnt-instance list"""
716   qa_utils.GenericQueryTest("gnt-instance", query.INSTANCE_FIELDS.keys())
717
718
719 def TestInstanceListFields():
720   """gnt-instance list-fields"""
721   qa_utils.GenericQueryFieldsTest("gnt-instance", query.INSTANCE_FIELDS.keys())
722
723
724 @InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
725 def TestInstanceConsole(instance):
726   """gnt-instance console"""
727   AssertCommand(["gnt-instance", "console", "--show-cmd", instance.name])
728
729
730 @InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
731 def TestReplaceDisks(instance, curr_nodes, other_nodes):
732   """gnt-instance replace-disks"""
733   def buildcmd(args):
734     cmd = ["gnt-instance", "replace-disks"]
735     cmd.extend(args)
736     cmd.append(instance.name)
737     return cmd
738
739   if not IsDiskReplacingSupported(instance):
740     print qa_utils.FormatInfo("Instance doesn't support disk replacing,"
741                               " skipping test")
742     return
743
744   # Currently all supported templates have one primary and one secondary node
745   assert len(curr_nodes) == 2
746   snode = curr_nodes[1]
747   assert len(other_nodes) == 1
748   othernode = other_nodes[0]
749
750   options = qa_config.get("options", {})
751   use_ialloc = options.get("use-iallocators", True)
752   for data in [
753     ["-p"],
754     ["-s"],
755     # A placeholder; the actual command choice depends on use_ialloc
756     None,
757     # Restore the original secondary
758     ["--new-secondary=%s" % snode.primary],
759     ]:
760     if data is None:
761       if use_ialloc:
762         data = ["-I", constants.DEFAULT_IALLOCATOR_SHORTCUT]
763       else:
764         data = ["--new-secondary=%s" % othernode.primary]
765     AssertCommand(buildcmd(data))
766
767   AssertCommand(buildcmd(["-a"]))
768   AssertCommand(["gnt-instance", "stop", instance.name])
769   AssertCommand(buildcmd(["-a"]), fail=True)
770   AssertCommand(["gnt-instance", "activate-disks", instance.name])
771   AssertCommand(["gnt-instance", "activate-disks", "--wait-for-sync",
772                  instance.name])
773   AssertCommand(buildcmd(["-a"]))
774   AssertCommand(["gnt-instance", "start", instance.name])
775
776
777 def _AssertRecreateDisks(cmdargs, instance, fail=False, check=True,
778                          destroy=True):
779   """Execute gnt-instance recreate-disks and check the result
780
781   @param cmdargs: Arguments (instance name excluded)
782   @param instance: Instance to operate on
783   @param fail: True if the command is expected to fail
784   @param check: If True and fail is False, check that the disks work
785   @prama destroy: If True, destroy the old disks first
786
787   """
788   if destroy:
789     _DestroyInstanceVolumes(instance)
790   AssertCommand((["gnt-instance", "recreate-disks"] + cmdargs +
791                  [instance.name]), fail)
792   if not fail and check:
793     # Quick check that the disks are there
794     AssertCommand(["gnt-instance", "activate-disks", instance.name])
795     AssertCommand(["gnt-instance", "activate-disks", "--wait-for-sync",
796                    instance.name])
797     AssertCommand(["gnt-instance", "deactivate-disks", instance.name])
798
799
800 @InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
801 def TestRecreateDisks(instance, inodes, othernodes):
802   """gnt-instance recreate-disks
803
804   @param instance: Instance to work on
805   @param inodes: List of the current nodes of the instance
806   @param othernodes: list/tuple of nodes where to temporarily recreate disks
807
808   """
809   options = qa_config.get("options", {})
810   use_ialloc = options.get("use-iallocators", True)
811   other_seq = ":".join([n.primary for n in othernodes])
812   orig_seq = ":".join([n.primary for n in inodes])
813   # These fail because the instance is running
814   _AssertRecreateDisks(["-n", other_seq], instance, fail=True, destroy=False)
815   if use_ialloc:
816     _AssertRecreateDisks(["-I", "hail"], instance, fail=True, destroy=False)
817   else:
818     _AssertRecreateDisks(["-n", other_seq], instance, fail=True, destroy=False)
819   AssertCommand(["gnt-instance", "stop", instance.name])
820   # Disks exist: this should fail
821   _AssertRecreateDisks([], instance, fail=True, destroy=False)
822   # Recreate disks in place
823   _AssertRecreateDisks([], instance)
824   # Move disks away
825   if use_ialloc:
826     _AssertRecreateDisks(["-I", "hail"], instance)
827     # Move disks somewhere else
828     _AssertRecreateDisks(["-I", constants.DEFAULT_IALLOCATOR_SHORTCUT],
829                          instance)
830   else:
831     _AssertRecreateDisks(["-n", other_seq], instance)
832   # Move disks back
833   _AssertRecreateDisks(["-n", orig_seq], instance, check=False)
834   # This and InstanceCheck decoration check that the disks are working
835   AssertCommand(["gnt-instance", "reinstall", "-f", instance.name])
836   AssertCommand(["gnt-instance", "start", instance.name])
837
838
839 @InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
840 def TestInstanceExport(instance, node):
841   """gnt-backup export -n ..."""
842   name = instance.name
843   AssertCommand(["gnt-backup", "export", "-n", node.primary, name])
844   return qa_utils.ResolveInstanceName(name)
845
846
847 @InstanceCheck(None, INST_DOWN, FIRST_ARG)
848 def TestInstanceExportWithRemove(instance, node):
849   """gnt-backup export --remove-instance"""
850   AssertCommand(["gnt-backup", "export", "-n", node.primary,
851                  "--remove-instance", instance.name])
852
853
854 @InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
855 def TestInstanceExportNoTarget(instance):
856   """gnt-backup export (without target node, should fail)"""
857   AssertCommand(["gnt-backup", "export", instance.name], fail=True)
858
859
860 @InstanceCheck(None, INST_DOWN, FIRST_ARG)
861 def TestInstanceImport(newinst, node, expnode, name):
862   """gnt-backup import"""
863   templ = constants.DT_PLAIN
864   cmd = (["gnt-backup", "import",
865           "--disk-template=%s" % templ,
866           "--no-ip-check",
867           "--src-node=%s" % expnode.primary,
868           "--src-dir=%s/%s" % (pathutils.EXPORT_DIR, name),
869           "--node=%s" % node.primary] +
870          _GetGenericAddParameters(newinst, templ,
871                                   force_mac=constants.VALUE_GENERATE))
872   cmd.append(newinst.name)
873   AssertCommand(cmd)
874   newinst.SetDiskTemplate(templ)
875
876
877 def TestBackupList(expnode):
878   """gnt-backup list"""
879   AssertCommand(["gnt-backup", "list", "--node=%s" % expnode.primary])
880
881   qa_utils.GenericQueryTest("gnt-backup", query.EXPORT_FIELDS.keys(),
882                             namefield=None, test_unknown=False)
883
884
885 def TestBackupListFields():
886   """gnt-backup list-fields"""
887   qa_utils.GenericQueryFieldsTest("gnt-backup", query.EXPORT_FIELDS.keys())
888
889
890 def TestRemoveInstanceOfflineNode(instance, snode, set_offline, set_online):
891   """gtn-instance remove with an off-line node
892
893   @param instance: instance
894   @param snode: secondary node, to be set offline
895   @param set_offline: function to call to set the node off-line
896   @param set_online: function to call to set the node on-line
897
898   """
899   info = _GetInstanceInfo(instance.name)
900   set_offline(snode)
901   try:
902     TestInstanceRemove(instance)
903   finally:
904     set_online(snode)
905   # Clean up the disks on the offline node
906   for minor in info["drbd-minors"][snode.primary]:
907     AssertCommand(["drbdsetup", str(minor), "down"], node=snode)
908   AssertCommand(["lvremove", "-f"] + info["volumes"], node=snode)