4 # Copyright (C) 2007, 2010, 2011, 2012, 2013 Google Inc.
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.
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.
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
22 """Cluster related QA tests.
30 from ganeti import constants
31 from ganeti import compat
32 from ganeti import utils
33 from ganeti import pathutils
39 from qa_utils import AssertEqual, AssertCommand, GetCommandOutput
42 # Prefix for LVM volumes created by QA code during tests
45 #: cluster verify command
46 _CLUSTER_VERIFY = ["gnt-cluster", "verify"]
49 def _RemoveFileFromAllNodes(filename):
50 """Removes a file from all nodes.
53 for node in qa_config.get("nodes"):
54 AssertCommand(["rm", "-f", filename], node=node)
57 def _CheckFileOnAllNodes(filename, content):
58 """Verifies the content of the given file on all nodes.
61 cmd = utils.ShellQuoteArgs(["cat", filename])
62 for node in qa_config.get("nodes"):
63 AssertEqual(qa_utils.GetCommandOutput(node["primary"], cmd), content)
66 # "gnt-cluster info" fields
67 _CIFIELD_RE = re.compile(r"^[-\s]*(?P<field>[^\s:]+):\s*(?P<value>\S.*)$")
70 def _GetBoolClusterField(field):
71 """Get the Boolean value of a cluster field.
73 This function currently assumes that the field name is unique in the cluster
74 configuration. An assertion checks this assumption.
77 @param field: Name of the field
79 @return: The effective value of the field
82 master = qa_config.GetMasterNode()
83 infocmd = "gnt-cluster info"
84 info_out = qa_utils.GetCommandOutput(master["primary"], infocmd)
86 for l in info_out.splitlines():
87 m = _CIFIELD_RE.match(l)
88 # FIXME: There should be a way to specify a field through a hierarchy
89 if m and m.group("field") == field:
90 # Make sure that ignoring the hierarchy doesn't cause a double match
92 ret = (m.group("value").lower() == "true")
95 raise qa_error.Error("Field not found in cluster configuration: %s" % field)
98 # Cluster-verify errors (date, "ERROR", then error code)
99 _CVERROR_RE = re.compile(r"^[\w\s:]+\s+- ERROR:([A-Z0-9_-]+):")
102 def _GetCVErrorCodes(cvout):
104 for l in cvout.splitlines():
105 m = _CVERROR_RE.match(l)
112 def AssertClusterVerify(fail=False, errors=None):
113 """Run cluster-verify and check the result
116 @param fail: if cluster-verify is expected to fail instead of succeeding
117 @type errors: list of tuples
118 @param errors: List of CV_XXX errors that are expected; if specified, all the
119 errors listed must appear in cluster-verify output. A non-empty value
120 implies C{fail=True}.
123 cvcmd = "gnt-cluster verify"
124 mnode = qa_config.GetMasterNode()
126 cvout = GetCommandOutput(mnode["primary"], cvcmd + " --error-codes",
128 actual = _GetCVErrorCodes(cvout)
129 expected = compat.UniqueFrozenset(e for (_, e, _) in errors)
130 if not actual.issuperset(expected):
131 missing = expected.difference(actual)
132 raise qa_error.Error("Cluster-verify didn't return these expected"
133 " errors: %s" % utils.CommaJoin(missing))
135 AssertCommand(cvcmd, fail=fail, node=mnode)
138 # data for testing failures due to bad keys/values for disk parameters
139 _FAIL_PARAMS = ["nonexistent:resync-rate=1",
140 "drbd:nonexistent=1",
141 "drbd:resync-rate=invalid",
145 def TestClusterInitDisk():
146 """gnt-cluster init -D"""
147 name = qa_config.get("name")
148 for param in _FAIL_PARAMS:
149 AssertCommand(["gnt-cluster", "init", "-D", param, name], fail=True)
152 def TestClusterInit(rapi_user, rapi_secret):
153 """gnt-cluster init"""
154 master = qa_config.GetMasterNode()
156 rapi_dir = os.path.dirname(pathutils.RAPI_USERS_FILE)
158 # First create the RAPI credentials
159 fh = tempfile.NamedTemporaryFile()
161 fh.write("%s %s write\n" % (rapi_user, rapi_secret))
164 tmpru = qa_utils.UploadFile(master["primary"], fh.name)
166 AssertCommand(["mkdir", "-p", rapi_dir])
167 AssertCommand(["mv", tmpru, pathutils.RAPI_USERS_FILE])
169 AssertCommand(["rm", "-f", tmpru])
175 "gnt-cluster", "init",
176 "--primary-ip-version=%d" % qa_config.get("primary_ip_version", 4),
177 "--enabled-hypervisors=%s" % ",".join(qa_config.GetEnabledHypervisors()),
180 for spec_type in ("mem-size", "disk-size", "disk-count", "cpu-count",
182 for spec_val in ("min", "max", "std"):
183 spec = qa_config.get("ispec_%s_%s" %
184 (spec_type.replace("-", "_"), spec_val), None)
186 cmd.append("--specs-%s=%s=%d" % (spec_type, spec_val, spec))
188 if master.get("secondary", None):
189 cmd.append("--secondary-ip=%s" % master["secondary"])
191 vgname = qa_config.get("vg-name", None)
193 cmd.append("--vg-name=%s" % vgname)
195 master_netdev = qa_config.get("master-netdev", None)
197 cmd.append("--master-netdev=%s" % master_netdev)
199 nicparams = qa_config.get("default-nicparams", None)
201 cmd.append("--nic-parameters=%s" %
202 ",".join(utils.FormatKeyValue(nicparams)))
204 # Cluster value of the exclusive-storage node parameter
205 e_s = qa_config.get("exclusive-storage")
207 cmd.extend(["--node-parameters", "exclusive_storage=%s" % e_s])
210 qa_config.SetExclusiveStorage(e_s)
212 cmd.append(qa_config.get("name"))
215 cmd = ["gnt-cluster", "modify"]
217 # hypervisor parameter modifications
218 hvp = qa_config.get("hypervisor-parameters", {})
219 for k, v in hvp.items():
220 cmd.extend(["-H", "%s:%s" % (k, v)])
221 # backend parameter modifications
222 bep = qa_config.get("backend-parameters", "")
224 cmd.extend(["-B", bep])
230 osp = qa_config.get("os-parameters", {})
231 for k, v in osp.items():
232 AssertCommand(["gnt-os", "modify", "-O", v, k])
234 # OS hypervisor parameters
235 os_hvp = qa_config.get("os-hvp", {})
236 for os_name in os_hvp:
237 for hv, hvp in os_hvp[os_name].items():
238 AssertCommand(["gnt-os", "modify", "-H", "%s:%s" % (hv, hvp), os_name])
241 def TestClusterRename():
242 """gnt-cluster rename"""
243 cmd = ["gnt-cluster", "rename", "-f"]
245 original_name = qa_config.get("name")
246 rename_target = qa_config.get("rename", None)
247 if rename_target is None:
248 print qa_utils.FormatError('"rename" entry is missing')
252 cmd + [rename_target],
254 cmd + [original_name],
260 def TestClusterOob():
261 """out-of-band framework"""
262 oob_path_exists = "/tmp/ganeti-qa-oob-does-exist-%s" % utils.NewUUID()
264 AssertCommand(_CLUSTER_VERIFY)
265 AssertCommand(["gnt-cluster", "modify", "--node-parameters",
266 "oob_program=/tmp/ganeti-qa-oob-does-not-exist-%s" %
269 AssertCommand(_CLUSTER_VERIFY, fail=True)
271 AssertCommand(["touch", oob_path_exists])
272 AssertCommand(["chmod", "0400", oob_path_exists])
273 AssertCommand(["gnt-cluster", "copyfile", oob_path_exists])
276 AssertCommand(["gnt-cluster", "modify", "--node-parameters",
277 "oob_program=%s" % oob_path_exists])
279 AssertCommand(_CLUSTER_VERIFY, fail=True)
281 AssertCommand(["chmod", "0500", oob_path_exists])
282 AssertCommand(["gnt-cluster", "copyfile", oob_path_exists])
284 AssertCommand(_CLUSTER_VERIFY)
286 AssertCommand(["gnt-cluster", "command", "rm", oob_path_exists])
288 AssertCommand(["gnt-cluster", "modify", "--node-parameters",
292 def TestClusterEpo():
293 """gnt-cluster epo"""
294 master = qa_config.GetMasterNode()
296 # Assert that OOB is unavailable for all nodes
297 result_output = GetCommandOutput(master["primary"],
298 "gnt-node list --verbose --no-headers -o"
300 AssertEqual(compat.all(powered == "(unavail)"
301 for powered in result_output.splitlines()), True)
304 AssertCommand(["gnt-cluster", "epo", "--groups", "--all"], fail=True)
305 # --all doesn't expect arguments
306 AssertCommand(["gnt-cluster", "epo", "--all", "some_arg"], fail=True)
308 # Unless --all is given master is not allowed to be in the list
309 AssertCommand(["gnt-cluster", "epo", "-f", master["primary"]], fail=True)
311 # This shouldn't fail
312 AssertCommand(["gnt-cluster", "epo", "-f", "--all"])
314 # All instances should have been stopped now
315 result_output = GetCommandOutput(master["primary"],
316 "gnt-instance list --no-headers -o status")
317 # ERROR_down because the instance is stopped but not recorded as such
318 AssertEqual(compat.all(status == "ERROR_down"
319 for status in result_output.splitlines()), True)
321 # Now start everything again
322 AssertCommand(["gnt-cluster", "epo", "--on", "-f", "--all"])
324 # All instances should have been started now
325 result_output = GetCommandOutput(master["primary"],
326 "gnt-instance list --no-headers -o status")
327 AssertEqual(compat.all(status == "running"
328 for status in result_output.splitlines()), True)
331 def TestClusterVerify():
332 """gnt-cluster verify"""
333 AssertCommand(_CLUSTER_VERIFY)
334 AssertCommand(["gnt-cluster", "verify-disks"])
338 """gnt-debug test-jobqueue"""
339 AssertCommand(["gnt-debug", "test-jobqueue"])
343 """gnt-debug delay"""
344 AssertCommand(["gnt-debug", "delay", "1"])
345 AssertCommand(["gnt-debug", "delay", "--no-master", "1"])
346 AssertCommand(["gnt-debug", "delay", "--no-master",
347 "-n", node["primary"], "1"])
350 def TestClusterReservedLvs():
351 """gnt-cluster reserved lvs"""
352 vgname = qa_config.get("vg-name", constants.DEFAULT_VG)
353 lvname = _QA_LV_PREFIX + "test"
354 lvfullname = "/".join([vgname, lvname])
356 (False, _CLUSTER_VERIFY),
357 (False, ["gnt-cluster", "modify", "--reserved-lvs", ""]),
358 (False, ["lvcreate", "-L1G", "-n", lvname, vgname]),
359 (True, _CLUSTER_VERIFY),
360 (False, ["gnt-cluster", "modify", "--reserved-lvs",
361 "%s,.*/other-test" % lvfullname]),
362 (False, _CLUSTER_VERIFY),
363 (False, ["gnt-cluster", "modify", "--reserved-lvs",
364 ".*/%s.*" % _QA_LV_PREFIX]),
365 (False, _CLUSTER_VERIFY),
366 (False, ["gnt-cluster", "modify", "--reserved-lvs", ""]),
367 (True, _CLUSTER_VERIFY),
368 (False, ["lvremove", "-f", lvfullname]),
369 (False, _CLUSTER_VERIFY),
371 AssertCommand(cmd, fail=fail)
374 def TestClusterModifyEmpty():
375 """gnt-cluster modify"""
376 AssertCommand(["gnt-cluster", "modify"], fail=True)
379 def TestClusterModifyDisk():
380 """gnt-cluster modify -D"""
381 for param in _FAIL_PARAMS:
382 AssertCommand(["gnt-cluster", "modify", "-D", param], fail=True)
385 def TestClusterModifyBe():
386 """gnt-cluster modify -B"""
389 (False, ["gnt-cluster", "modify", "-B", "maxmem=256"]),
390 (False, ["sh", "-c", "gnt-cluster info|grep '^ *maxmem: 256$'"]),
391 (False, ["gnt-cluster", "modify", "-B", "minmem=256"]),
392 (False, ["sh", "-c", "gnt-cluster info|grep '^ *minmem: 256$'"]),
393 (True, ["gnt-cluster", "modify", "-B", "maxmem=a"]),
394 (False, ["sh", "-c", "gnt-cluster info|grep '^ *maxmem: 256$'"]),
395 (True, ["gnt-cluster", "modify", "-B", "minmem=a"]),
396 (False, ["sh", "-c", "gnt-cluster info|grep '^ *minmem: 256$'"]),
397 (False, ["gnt-cluster", "modify", "-B", "maxmem=128,minmem=128"]),
398 (False, ["sh", "-c", "gnt-cluster info|grep '^ *maxmem: 128$'"]),
399 (False, ["sh", "-c", "gnt-cluster info|grep '^ *minmem: 128$'"]),
401 (False, ["gnt-cluster", "modify", "-B", "vcpus=4"]),
402 (False, ["sh", "-c", "gnt-cluster info|grep '^ *vcpus: 4$'"]),
403 (True, ["gnt-cluster", "modify", "-B", "vcpus=a"]),
404 (False, ["gnt-cluster", "modify", "-B", "vcpus=1"]),
405 (False, ["sh", "-c", "gnt-cluster info|grep '^ *vcpus: 1$'"]),
407 (False, ["gnt-cluster", "modify", "-B", "auto_balance=False"]),
408 (False, ["sh", "-c", "gnt-cluster info|grep '^ *auto_balance: False$'"]),
409 (True, ["gnt-cluster", "modify", "-B", "auto_balance=1"]),
410 (False, ["gnt-cluster", "modify", "-B", "auto_balance=True"]),
411 (False, ["sh", "-c", "gnt-cluster info|grep '^ *auto_balance: True$'"]),
413 AssertCommand(cmd, fail=fail)
415 # redo the original-requested BE parameters, if any
416 bep = qa_config.get("backend-parameters", "")
418 AssertCommand(["gnt-cluster", "modify", "-B", bep])
421 def TestClusterInfo():
422 """gnt-cluster info"""
423 AssertCommand(["gnt-cluster", "info"])
426 def TestClusterRedistConf():
427 """gnt-cluster redist-conf"""
428 AssertCommand(["gnt-cluster", "redist-conf"])
431 def TestClusterGetmaster():
432 """gnt-cluster getmaster"""
433 AssertCommand(["gnt-cluster", "getmaster"])
436 def TestClusterVersion():
437 """gnt-cluster version"""
438 AssertCommand(["gnt-cluster", "version"])
441 def TestClusterRenewCrypto():
442 """gnt-cluster renew-crypto"""
443 master = qa_config.GetMasterNode()
445 # Conflicting options
446 cmd = ["gnt-cluster", "renew-crypto", "--force",
447 "--new-cluster-certificate", "--new-confd-hmac-key"]
449 ["--new-rapi-certificate", "--rapi-certificate=/dev/null"],
450 ["--new-cluster-domain-secret", "--cluster-domain-secret=/dev/null"],
452 for i in conflicting:
453 AssertCommand(cmd + i, fail=True)
455 # Invalid RAPI certificate
456 cmd = ["gnt-cluster", "renew-crypto", "--force",
457 "--rapi-certificate=/dev/null"]
458 AssertCommand(cmd, fail=True)
460 rapi_cert_backup = qa_utils.BackupFile(master["primary"],
461 pathutils.RAPI_CERT_FILE)
463 # Custom RAPI certificate
464 fh = tempfile.NamedTemporaryFile()
466 # Ensure certificate doesn't cause "gnt-cluster verify" to complain
467 validity = constants.SSL_CERT_EXPIRATION_WARN * 3
469 utils.GenerateSelfSignedSslCert(fh.name, validity=validity)
471 tmpcert = qa_utils.UploadFile(master["primary"], fh.name)
473 AssertCommand(["gnt-cluster", "renew-crypto", "--force",
474 "--rapi-certificate=%s" % tmpcert])
476 AssertCommand(["rm", "-f", tmpcert])
478 # Custom cluster domain secret
479 cds_fh = tempfile.NamedTemporaryFile()
480 cds_fh.write(utils.GenerateSecret())
484 tmpcds = qa_utils.UploadFile(master["primary"], cds_fh.name)
486 AssertCommand(["gnt-cluster", "renew-crypto", "--force",
487 "--cluster-domain-secret=%s" % tmpcds])
489 AssertCommand(["rm", "-f", tmpcds])
492 AssertCommand(["gnt-cluster", "renew-crypto", "--force",
493 "--new-cluster-certificate", "--new-confd-hmac-key",
494 "--new-rapi-certificate", "--new-cluster-domain-secret"])
496 # Restore RAPI certificate
497 AssertCommand(["gnt-cluster", "renew-crypto", "--force",
498 "--rapi-certificate=%s" % rapi_cert_backup])
500 AssertCommand(["rm", "-f", rapi_cert_backup])
503 def TestClusterBurnin():
505 master = qa_config.GetMasterNode()
507 options = qa_config.get("options", {})
508 disk_template = options.get("burnin-disk-template", "drbd")
509 parallel = options.get("burnin-in-parallel", False)
510 check_inst = options.get("burnin-check-instances", False)
511 do_rename = options.get("burnin-rename", "")
512 do_reboot = options.get("burnin-reboot", True)
513 reboot_types = options.get("reboot-types", constants.REBOOT_TYPES)
515 # Get as many instances as we need
519 num = qa_config.get("options", {}).get("burnin-instances", 1)
520 for _ in range(0, num):
521 instances.append(qa_config.AcquireInstance())
522 except qa_error.OutOfInstancesError:
523 print "Not enough instances, continuing anyway."
525 if len(instances) < 1:
526 raise qa_error.Error("Burnin needs at least one instance")
528 script = qa_utils.UploadFile(master["primary"], "../tools/burnin")
532 "--os=%s" % qa_config.get("os"),
533 "--minmem-size=%s" % qa_config.get(constants.BE_MINMEM),
534 "--maxmem-size=%s" % qa_config.get(constants.BE_MAXMEM),
535 "--disk-size=%s" % ",".join(qa_config.get("disk")),
536 "--disk-growth=%s" % ",".join(qa_config.get("disk-growth")),
537 "--disk-template=%s" % disk_template]
539 cmd.append("--parallel")
540 cmd.append("--early-release")
542 cmd.append("--http-check")
544 cmd.append("--rename=%s" % do_rename)
546 cmd.append("--no-reboot")
548 cmd.append("--reboot-types=%s" % ",".join(reboot_types))
549 cmd += [inst["name"] for inst in instances]
552 AssertCommand(["rm", "-f", script])
555 for inst in instances:
556 qa_config.ReleaseInstance(inst)
559 def TestClusterMasterFailover():
560 """gnt-cluster master-failover"""
561 master = qa_config.GetMasterNode()
562 failovermaster = qa_config.AcquireNode(exclude=master)
564 cmd = ["gnt-cluster", "master-failover"]
566 AssertCommand(cmd, node=failovermaster)
567 # Back to original master node
568 AssertCommand(cmd, node=master)
570 qa_config.ReleaseNode(failovermaster)
573 def TestClusterMasterFailoverWithDrainedQueue():
574 """gnt-cluster master-failover with drained queue"""
575 drain_check = ["test", "-f", pathutils.JOB_QUEUE_DRAIN_FILE]
577 master = qa_config.GetMasterNode()
578 failovermaster = qa_config.AcquireNode(exclude=master)
580 # Ensure queue is not drained
581 for node in [master, failovermaster]:
582 AssertCommand(drain_check, node=node, fail=True)
584 # Drain queue on failover master
585 AssertCommand(["touch", pathutils.JOB_QUEUE_DRAIN_FILE], node=failovermaster)
587 cmd = ["gnt-cluster", "master-failover"]
589 AssertCommand(drain_check, node=failovermaster)
590 AssertCommand(cmd, node=failovermaster)
591 AssertCommand(drain_check, fail=True)
592 AssertCommand(drain_check, node=failovermaster, fail=True)
594 # Back to original master node
595 AssertCommand(cmd, node=master)
597 qa_config.ReleaseNode(failovermaster)
599 AssertCommand(drain_check, fail=True)
600 AssertCommand(drain_check, node=failovermaster, fail=True)
603 def TestClusterCopyfile():
604 """gnt-cluster copyfile"""
605 master = qa_config.GetMasterNode()
607 uniqueid = utils.NewUUID()
609 # Create temporary file
610 f = tempfile.NamedTemporaryFile()
615 # Upload file to master node
616 testname = qa_utils.UploadFile(master["primary"], f.name)
618 # Copy file to all nodes
619 AssertCommand(["gnt-cluster", "copyfile", testname])
620 _CheckFileOnAllNodes(testname, uniqueid)
622 _RemoveFileFromAllNodes(testname)
625 def TestClusterCommand():
626 """gnt-cluster command"""
627 uniqueid = utils.NewUUID()
628 rfile = "/tmp/gnt%s" % utils.NewUUID()
629 rcmd = utils.ShellQuoteArgs(["echo", "-n", uniqueid])
630 cmd = utils.ShellQuoteArgs(["gnt-cluster", "command",
631 "%s >%s" % (rcmd, rfile)])
635 _CheckFileOnAllNodes(rfile, uniqueid)
637 _RemoveFileFromAllNodes(rfile)
640 def TestClusterDestroy():
641 """gnt-cluster destroy"""
642 AssertCommand(["gnt-cluster", "destroy", "--yes-do-it"])
645 def TestClusterRepairDiskSizes():
646 """gnt-cluster repair-disk-sizes"""
647 AssertCommand(["gnt-cluster", "repair-disk-sizes"])
650 def TestSetExclStorCluster(newvalue):
651 """Set the exclusive_storage node parameter at the cluster level.
654 @param newvalue: New value of exclusive_storage
656 @return: The old value of exclusive_storage
659 oldvalue = _GetBoolClusterField("exclusive_storage")
660 AssertCommand(["gnt-cluster", "modify", "--node-parameters",
661 "exclusive_storage=%s" % newvalue])
662 effvalue = _GetBoolClusterField("exclusive_storage")
663 if effvalue != newvalue:
664 raise qa_error.Error("exclusive_storage has the wrong value: %s instead"
665 " of %s" % (effvalue, newvalue))
666 qa_config.SetExclusiveStorage(newvalue)
670 def _BuildSetESCmd(value, node_name):
671 return ["gnt-node", "modify", "--node-parameters",
672 "exclusive_storage=%s" % value, node_name]
675 def TestExclStorSingleNode(node):
676 """cluster-verify reports exclusive_storage set only on one node.
679 node_name = node["primary"]
680 es_val = _GetBoolClusterField("exclusive_storage")
682 AssertCommand(_BuildSetESCmd(True, node_name))
683 AssertClusterVerify(fail=True, errors=[constants.CV_EGROUPMIXEDESFLAG])
684 AssertCommand(_BuildSetESCmd("default", node_name))
685 AssertClusterVerify()
688 def TestExclStorSharedPv(node):
689 """cluster-verify reports LVs that share the same PV with exclusive_storage.
692 vgname = qa_config.get("vg-name", constants.DEFAULT_VG)
693 lvname1 = _QA_LV_PREFIX + "vol1"
694 lvname2 = _QA_LV_PREFIX + "vol2"
695 node_name = node["primary"]
696 AssertCommand(["lvcreate", "-L1G", "-n", lvname1, vgname], node=node_name)
697 AssertClusterVerify(fail=True, errors=[constants.CV_ENODEORPHANLV])
698 AssertCommand(["lvcreate", "-L1G", "-n", lvname2, vgname], node=node_name)
699 AssertClusterVerify(fail=True, errors=[constants.CV_ENODELVM,
700 constants.CV_ENODEORPHANLV])
701 AssertCommand(["lvremove", "-f", "/".join([vgname, lvname1])], node=node_name)
702 AssertCommand(["lvremove", "-f", "/".join([vgname, lvname2])], node=node_name)
703 AssertClusterVerify()