Statistics
| Branch: | Tag: | Revision:

root / qa / qa_instance.py @ 615551b2

History | View | Annotate | Download (38.8 kB)

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-template=%s" %
1031
       ",".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-template=%s" %
1058
                       ",".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-template=%s" %
1071
                     ",".join(other_disk_templates)],
1072
                  fail=False)
1073
    CreateInstanceByDiskTemplate(nodes, enabled_disk_templates[0], fail=True)
1074
  else:
1075
    raise qa_error.Error("Please enable at least one disk template"
1076
                         " in your QA setup.")
1077

    
1078
  # Restore initially enabled disk templates
1079
  AssertCommand(["gnt-cluster", "modify",
1080
                 "--enabled-disk-template=%s" %
1081
                   ",".join(enabled_disk_templates)],
1082
                 fail=False)