4 # Copyright (C) 2007, 2008, 2009, 2010, 2011, 2012 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 """Remote API QA tests.
31 from ganeti import utils
32 from ganeti import constants
33 from ganeti import errors
34 from ganeti import cli
35 from ganeti import rapi
36 from ganeti import objects
37 from ganeti import query
38 from ganeti import compat
39 from ganeti import qlang
41 import ganeti.rapi.client # pylint: disable=W0611
42 import ganeti.rapi.client_utils
48 from qa_utils import (AssertEqual, AssertIn, AssertMatch, StartLocalCommand)
49 from qa_utils import InstanceCheck, INST_DOWN, INST_UP, FIRST_ARG
58 def Setup(username, password):
59 """Configures the RAPI client.
62 # pylint: disable=W0603
69 _rapi_username = username
70 _rapi_password = password
72 master = qa_config.GetMasterNode()
74 # Load RAPI certificate from master node
75 cmd = ["cat", constants.RAPI_CERT_FILE]
77 # Write to temporary file
78 _rapi_ca = tempfile.NamedTemporaryFile()
79 _rapi_ca.write(qa_utils.GetCommandOutput(master["primary"],
80 utils.ShellQuoteArgs(cmd)))
83 port = qa_config.get("rapi-port", default=constants.DEFAULT_RAPI_PORT)
84 cfg_curl = rapi.client.GenericCurlConfig(cafile=_rapi_ca.name,
87 _rapi_client = rapi.client.GanetiRapiClient(master["primary"], port=port,
90 curl_config_fn=cfg_curl)
92 print "RAPI protocol version: %s" % _rapi_client.GetVersion()
95 INSTANCE_FIELDS = ("name", "os", "pnode", "snodes",
97 "disk_template", "disk.sizes",
98 "nic.ips", "nic.macs", "nic.modes", "nic.links",
99 "beparams", "hvparams",
100 "oper_state", "oper_ram", "oper_vcpus", "status", "tags")
102 NODE_FIELDS = ("name", "dtotal", "dfree",
103 "mtotal", "mnode", "mfree",
104 "pinst_cnt", "sinst_cnt", "tags")
106 GROUP_FIELDS = frozenset([
109 "node_cnt", "node_list",
112 JOB_FIELDS = frozenset([
113 "id", "ops", "status", "summary",
114 "opstatus", "opresult", "oplog",
115 "received_ts", "start_ts", "end_ts",
118 LIST_FIELDS = ("id", "uri")
122 """Return whether remote API tests should be run.
125 return qa_config.TestEnabled("rapi")
129 # pylint: disable=W0212
130 # due to _SendRequest usage
133 for uri, verify, method, body in uris:
134 assert uri.startswith("/")
136 print "%s %s" % (method, uri)
137 data = _rapi_client._SendRequest(method, uri, None, body)
139 if verify is not None:
143 AssertEqual(data, verify)
150 def _VerifyReturnsJob(data):
151 if not isinstance(data, int):
152 AssertMatch(data, r"^\d+$")
156 """Testing remote API version.
160 ("/version", constants.RAPI_VERSION, "GET", None),
164 def TestEmptyCluster():
165 """Testing remote API on an empty cluster.
168 master = qa_config.GetMasterNode()
169 master_full = qa_utils.ResolveNodeName(master)
171 def _VerifyInfo(data):
172 AssertIn("name", data)
173 AssertIn("master", data)
174 AssertEqual(data["master"], master_full)
176 def _VerifyNodes(data):
179 "uri": "/2/nodes/%s" % master_full,
181 AssertIn(master_entry, data)
183 def _VerifyNodesBulk(data):
185 for entry in NODE_FIELDS:
186 AssertIn(entry, node)
188 def _VerifyGroups(data):
190 "name": constants.INITIAL_NODE_GROUP_NAME,
191 "uri": "/2/groups/" + constants.INITIAL_NODE_GROUP_NAME,
193 AssertIn(default_group, data)
195 def _VerifyGroupsBulk(data):
197 for field in GROUP_FIELDS:
198 AssertIn(field, group)
201 ("/", None, "GET", None),
202 ("/2/info", _VerifyInfo, "GET", None),
203 ("/2/tags", None, "GET", None),
204 ("/2/nodes", _VerifyNodes, "GET", None),
205 ("/2/nodes?bulk=1", _VerifyNodesBulk, "GET", None),
206 ("/2/groups", _VerifyGroups, "GET", None),
207 ("/2/groups?bulk=1", _VerifyGroupsBulk, "GET", None),
208 ("/2/instances", [], "GET", None),
209 ("/2/instances?bulk=1", [], "GET", None),
210 ("/2/os", None, "GET", None),
213 # Test HTTP Not Found
214 for method in ["GET", "PUT", "POST", "DELETE"]:
216 _DoTests([("/99/resource/not/here/99", None, method, None)])
217 except rapi.client.GanetiApiError, err:
218 AssertEqual(err.code, 404)
220 raise qa_error.Error("Non-existent resource didn't return HTTP 404")
222 # Test HTTP Not Implemented
223 for method in ["PUT", "POST", "DELETE"]:
225 _DoTests([("/version", None, method, None)])
226 except rapi.client.GanetiApiError, err:
227 AssertEqual(err.code, 501)
229 raise qa_error.Error("Non-implemented method didn't fail")
233 """Testing resource queries via remote API.
236 master_name = qa_utils.ResolveNodeName(qa_config.GetMasterNode())
237 rnd = random.Random(7818)
239 for what in constants.QR_VIA_RAPI:
240 if what == constants.QR_JOB:
242 elif what == constants.QR_EXPORT:
247 all_fields = query.ALL_FIELDS[what].keys()
248 rnd.shuffle(all_fields)
250 # No fields, should return everything
251 result = _rapi_client.QueryFields(what)
252 qresult = objects.QueryFieldsResponse.FromDict(result)
253 AssertEqual(len(qresult.fields), len(all_fields))
256 result = _rapi_client.QueryFields(what, fields=[namefield])
257 qresult = objects.QueryFieldsResponse.FromDict(result)
258 AssertEqual(len(qresult.fields), 1)
260 # Specify all fields, order must be correct
261 result = _rapi_client.QueryFields(what, fields=all_fields)
262 qresult = objects.QueryFieldsResponse.FromDict(result)
263 AssertEqual(len(qresult.fields), len(all_fields))
264 AssertEqual([fdef.name for fdef in qresult.fields], all_fields)
267 result = _rapi_client.QueryFields(what, fields=["_unknown!"])
268 qresult = objects.QueryFieldsResponse.FromDict(result)
269 AssertEqual(len(qresult.fields), 1)
270 AssertEqual(qresult.fields[0].name, "_unknown!")
271 AssertEqual(qresult.fields[0].kind, constants.QFT_UNKNOWN)
273 # Try once more, this time without the client
275 ("/2/query/%s/fields" % what, None, "GET", None),
276 ("/2/query/%s/fields?fields=name,name,%s" % (what, all_fields[0]),
280 # Try missing query argument
283 ("/2/query/%s" % what, None, "GET", None),
285 except rapi.client.GanetiApiError, err:
286 AssertEqual(err.code, 400)
288 raise qa_error.Error("Request missing 'fields' parameter didn't fail")
290 def _Check(exp_fields, data):
291 qresult = objects.QueryResponse.FromDict(data)
292 AssertEqual([fdef.name for fdef in qresult.fields], exp_fields)
293 if not isinstance(qresult.data, list):
294 raise qa_error.Error("Query did not return a list")
297 # Specify fields in query
298 ("/2/query/%s?fields=%s" % (what, ",".join(all_fields)),
299 compat.partial(_Check, all_fields), "GET", None),
301 ("/2/query/%s?fields=%s" % (what, namefield),
302 compat.partial(_Check, [namefield]), "GET", None),
305 ("/2/query/%s?fields=%s,%%20%s%%09,%s%%20" %
306 (what, namefield, namefield, namefield),
307 compat.partial(_Check, [namefield] * 3), "GET", None),
309 # PUT with fields in query
310 ("/2/query/%s?fields=%s" % (what, namefield),
311 compat.partial(_Check, [namefield]), "PUT", {}),
314 ("/2/query/%s" % what, compat.partial(_Check, all_fields), "PUT", {
315 "fields": all_fields,
318 ("/2/query/%s" % what, compat.partial(_Check, [namefield] * 4), "PUT", {
319 "fields": [namefield] * 4,
326 ("/2/query/%s" % what, compat.partial(_Check, all_fields), "PUT", {
327 "fields": all_fields,
328 "filter": [qlang.OP_TRUE, namefield],
332 if what == constants.QR_LOCK:
333 # Locks can't be filtered
336 except rapi.client.GanetiApiError, err:
337 AssertEqual(err.code, 500)
339 raise qa_error.Error("Filtering locks didn't fail")
343 if what == constants.QR_NODE:
345 (nodes, ) = _DoTests(
346 [("/2/query/%s" % what,
347 compat.partial(_Check, ["name", "master"]), "PUT",
348 {"fields": ["name", "master"],
349 "filter": [qlang.OP_TRUE, "master"],
351 qresult = objects.QueryResponse.FromDict(nodes)
352 AssertEqual(qresult.data, [
353 [[constants.RS_NORMAL, master_name], [constants.RS_NORMAL, True]],
357 @InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
358 def TestInstance(instance):
359 """Testing getting instance(s) info via remote API.
362 def _VerifyInstance(data):
363 for entry in INSTANCE_FIELDS:
364 AssertIn(entry, data)
366 def _VerifyInstancesList(data):
367 for instance in data:
368 for entry in LIST_FIELDS:
369 AssertIn(entry, instance)
371 def _VerifyInstancesBulk(data):
372 for instance_data in data:
373 _VerifyInstance(instance_data)
376 ("/2/instances/%s" % instance["name"], _VerifyInstance, "GET", None),
377 ("/2/instances", _VerifyInstancesList, "GET", None),
378 ("/2/instances?bulk=1", _VerifyInstancesBulk, "GET", None),
379 ("/2/instances/%s/activate-disks" % instance["name"],
380 _VerifyReturnsJob, "PUT", None),
381 ("/2/instances/%s/deactivate-disks" % instance["name"],
382 _VerifyReturnsJob, "PUT", None),
385 # Test OpBackupPrepare
386 (job_id, ) = _DoTests([
387 ("/2/instances/%s/prepare-export?mode=%s" %
388 (instance["name"], constants.EXPORT_MODE_REMOTE),
389 _VerifyReturnsJob, "PUT", None),
392 result = _WaitForRapiJob(job_id)[0]
393 AssertEqual(len(result["handshake"]), 3)
394 AssertEqual(result["handshake"][0], constants.RIE_VERSION)
395 AssertEqual(len(result["x509_key_name"]), 3)
396 AssertIn("-----BEGIN CERTIFICATE-----", result["x509_ca"])
400 """Testing getting node(s) info via remote API.
403 def _VerifyNode(data):
404 for entry in NODE_FIELDS:
405 AssertIn(entry, data)
407 def _VerifyNodesList(data):
409 for entry in LIST_FIELDS:
410 AssertIn(entry, node)
412 def _VerifyNodesBulk(data):
413 for node_data in data:
414 _VerifyNode(node_data)
417 ("/2/nodes/%s" % node["primary"], _VerifyNode, "GET", None),
418 ("/2/nodes", _VerifyNodesList, "GET", None),
419 ("/2/nodes?bulk=1", _VerifyNodesBulk, "GET", None),
423 def _FilterTags(seq):
424 """Removes unwanted tags from a sequence.
427 ignore_re = qa_config.get("ignore-tags-re", None)
430 return itertools.ifilterfalse(re.compile(ignore_re).match, seq)
435 def TestTags(kind, name, tags):
436 """Tests .../tags resources.
439 if kind == constants.TAG_CLUSTER:
441 elif kind == constants.TAG_NODE:
442 uri = "/2/nodes/%s/tags" % name
443 elif kind == constants.TAG_INSTANCE:
444 uri = "/2/instances/%s/tags" % name
445 elif kind == constants.TAG_NODEGROUP:
446 uri = "/2/groups/%s/tags" % name
448 raise errors.ProgrammerError("Unknown tag kind")
450 def _VerifyTags(data):
451 AssertEqual(sorted(tags), sorted(_FilterTags(data)))
453 queryargs = "&".join("tag=%s" % i for i in tags)
456 (job_id, ) = _DoTests([
457 ("%s?%s" % (uri, queryargs), _VerifyReturnsJob, "PUT", None),
459 _WaitForRapiJob(job_id)
463 (uri, _VerifyTags, "GET", None),
467 (job_id, ) = _DoTests([
468 ("%s?%s" % (uri, queryargs), _VerifyReturnsJob, "DELETE", None),
470 _WaitForRapiJob(job_id)
473 def _WaitForRapiJob(job_id):
474 """Waits for a job to finish.
477 def _VerifyJob(data):
478 AssertEqual(data["id"], job_id)
479 for field in JOB_FIELDS:
480 AssertIn(field, data)
483 ("/2/jobs/%s" % job_id, _VerifyJob, "GET", None),
486 return rapi.client_utils.PollJob(_rapi_client, job_id,
487 cli.StdioJobPollReportCb())
490 def TestRapiNodeGroups():
491 """Test several node group operations using RAPI.
494 groups = qa_config.get("groups", {})
495 group1, group2, group3 = groups.get("inexistent-groups",
496 ["group1", "group2", "group3"])[:3]
498 # Create a group with no attributes
503 (job_id, ) = _DoTests([
504 ("/2/groups", _VerifyReturnsJob, "POST", body),
507 _WaitForRapiJob(job_id)
509 # Create a group specifying alloc_policy
512 "alloc_policy": constants.ALLOC_POLICY_UNALLOCABLE,
515 (job_id, ) = _DoTests([
516 ("/2/groups", _VerifyReturnsJob, "POST", body),
519 _WaitForRapiJob(job_id)
521 # Modify alloc_policy
523 "alloc_policy": constants.ALLOC_POLICY_UNALLOCABLE,
526 (job_id, ) = _DoTests([
527 ("/2/groups/%s/modify" % group1, _VerifyReturnsJob, "PUT", body),
530 _WaitForRapiJob(job_id)
537 (job_id, ) = _DoTests([
538 ("/2/groups/%s/rename" % group2, _VerifyReturnsJob, "PUT", body),
541 _WaitForRapiJob(job_id)
544 for group in [group1, group3]:
545 (job_id, ) = _DoTests([
546 ("/2/groups/%s" % group, _VerifyReturnsJob, "DELETE", None),
549 _WaitForRapiJob(job_id)
552 def TestRapiInstanceAdd(node, use_client):
553 """Test adding a new instance via RAPI"""
554 instance = qa_config.AcquireInstance()
556 disk_sizes = [utils.ParseUnit(size) for size in qa_config.get("disk")]
557 disks = [{"size": size} for size in disk_sizes]
558 nic0_mac = qa_config.GetInstanceNicMac(instance,
559 default=constants.VALUE_GENERATE)
561 constants.INIC_MAC: nic0_mac,
565 constants.BE_MAXMEM: utils.ParseUnit(qa_config.get(constants.BE_MAXMEM)),
566 constants.BE_MINMEM: utils.ParseUnit(qa_config.get(constants.BE_MINMEM)),
570 job_id = _rapi_client.CreateInstance(constants.INSTANCE_CREATE,
574 os=qa_config.get("os"),
575 pnode=node["primary"],
580 "mode": constants.INSTANCE_CREATE,
581 "name": instance["name"],
582 "os_type": qa_config.get("os"),
583 "disk_template": constants.DT_PLAIN,
584 "pnode": node["primary"],
585 "beparams": beparams,
590 (job_id, ) = _DoTests([
591 ("/2/instances", _VerifyReturnsJob, "POST", body),
594 _WaitForRapiJob(job_id)
598 qa_config.ReleaseInstance(instance)
602 @InstanceCheck(None, INST_DOWN, FIRST_ARG)
603 def TestRapiInstanceRemove(instance, use_client):
604 """Test removing instance via RAPI"""
606 job_id = _rapi_client.DeleteInstance(instance["name"])
608 (job_id, ) = _DoTests([
609 ("/2/instances/%s" % instance["name"], _VerifyReturnsJob, "DELETE", None),
612 _WaitForRapiJob(job_id)
614 qa_config.ReleaseInstance(instance)
617 @InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
618 def TestRapiInstanceMigrate(instance):
619 """Test migrating instance via RAPI"""
620 # Move to secondary node
621 _WaitForRapiJob(_rapi_client.MigrateInstance(instance["name"]))
622 qa_utils.RunInstanceCheck(instance, True)
623 # And back to previous primary
624 _WaitForRapiJob(_rapi_client.MigrateInstance(instance["name"]))
627 @InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
628 def TestRapiInstanceFailover(instance):
629 """Test failing over instance via RAPI"""
630 # Move to secondary node
631 _WaitForRapiJob(_rapi_client.FailoverInstance(instance["name"]))
632 qa_utils.RunInstanceCheck(instance, True)
633 # And back to previous primary
634 _WaitForRapiJob(_rapi_client.FailoverInstance(instance["name"]))
637 @InstanceCheck(INST_UP, INST_DOWN, FIRST_ARG)
638 def TestRapiInstanceShutdown(instance):
639 """Test stopping an instance via RAPI"""
640 _WaitForRapiJob(_rapi_client.ShutdownInstance(instance["name"]))
643 @InstanceCheck(INST_DOWN, INST_UP, FIRST_ARG)
644 def TestRapiInstanceStartup(instance):
645 """Test starting an instance via RAPI"""
646 _WaitForRapiJob(_rapi_client.StartupInstance(instance["name"]))
649 @InstanceCheck(INST_DOWN, INST_DOWN, FIRST_ARG)
650 def TestRapiInstanceRenameAndBack(rename_source, rename_target):
651 """Test renaming instance via RAPI
653 This must leave the instance with the original name (in the
657 _WaitForRapiJob(_rapi_client.RenameInstance(rename_source, rename_target))
658 qa_utils.RunInstanceCheck(rename_source, False)
659 qa_utils.RunInstanceCheck(rename_target, False)
660 _WaitForRapiJob(_rapi_client.RenameInstance(rename_target, rename_source))
661 qa_utils.RunInstanceCheck(rename_target, False)
664 @InstanceCheck(INST_DOWN, INST_DOWN, FIRST_ARG)
665 def TestRapiInstanceReinstall(instance):
666 """Test reinstalling an instance via RAPI"""
667 _WaitForRapiJob(_rapi_client.ReinstallInstance(instance["name"]))
668 # By default, the instance is started again
669 qa_utils.RunInstanceCheck(instance, True)
671 # Reinstall again without starting
672 _WaitForRapiJob(_rapi_client.ReinstallInstance(instance["name"],
676 @InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
677 def TestRapiInstanceReplaceDisks(instance):
678 """Test replacing instance disks via RAPI"""
679 fn = _rapi_client.ReplaceInstanceDisks
680 _WaitForRapiJob(fn(instance["name"],
681 mode=constants.REPLACE_DISK_AUTO, disks=[]))
682 _WaitForRapiJob(fn(instance["name"],
683 mode=constants.REPLACE_DISK_SEC, disks="0"))
686 @InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
687 def TestRapiInstanceModify(instance):
688 """Test modifying instance via RAPI"""
689 default_hv = qa_config.GetDefaultHypervisor()
691 def _ModifyInstance(**kwargs):
692 _WaitForRapiJob(_rapi_client.ModifyInstance(instance["name"], **kwargs))
694 _ModifyInstance(beparams={
695 constants.BE_VCPUS: 3,
698 _ModifyInstance(beparams={
699 constants.BE_VCPUS: constants.VALUE_DEFAULT,
702 if default_hv == constants.HT_XEN_PVM:
703 _ModifyInstance(hvparams={
704 constants.HV_KERNEL_ARGS: "single",
706 _ModifyInstance(hvparams={
707 constants.HV_KERNEL_ARGS: constants.VALUE_DEFAULT,
709 elif default_hv == constants.HT_XEN_HVM:
710 _ModifyInstance(hvparams={
711 constants.HV_BOOT_ORDER: "acn",
713 _ModifyInstance(hvparams={
714 constants.HV_BOOT_ORDER: constants.VALUE_DEFAULT,
718 @InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
719 def TestRapiInstanceConsole(instance):
720 """Test getting instance console information via RAPI"""
721 result = _rapi_client.GetInstanceConsole(instance["name"])
722 console = objects.InstanceConsole.FromDict(result)
723 AssertEqual(console.Validate(), True)
724 AssertEqual(console.instance, qa_utils.ResolveInstanceName(instance["name"]))
727 @InstanceCheck(INST_DOWN, INST_DOWN, FIRST_ARG)
728 def TestRapiStoppedInstanceConsole(instance):
729 """Test getting stopped instance's console information via RAPI"""
731 _rapi_client.GetInstanceConsole(instance["name"])
732 except rapi.client.GanetiApiError, err:
733 AssertEqual(err.code, 503)
735 raise qa_error.Error("Getting console for stopped instance didn't"
739 def GetOperatingSystems():
740 """Retrieves a list of all available operating systems.
743 return _rapi_client.GetOperatingSystems()
746 def TestInterClusterInstanceMove(src_instance, dest_instance,
747 pnode, snode, tnode):
748 """Test tools/move-instance"""
749 master = qa_config.GetMasterNode()
751 rapi_pw_file = tempfile.NamedTemporaryFile()
752 rapi_pw_file.write(_rapi_password)
755 # TODO: Run some instance tests before moving back
758 # instance is not redundant, but we still need to pass a node
759 # (which will be ignored)
763 # note: pnode:snode are the *current* nodes, so we move it first to
764 # tnode:pnode, then back to pnode:snode
765 for si, di, pn, sn in [(src_instance["name"], dest_instance["name"],
766 tnode["primary"], pnode["primary"]),
767 (dest_instance["name"], src_instance["name"],
768 pnode["primary"], fsec["primary"])]:
770 "../tools/move-instance",
772 "--src-ca-file=%s" % _rapi_ca.name,
773 "--src-username=%s" % _rapi_username,
774 "--src-password-file=%s" % rapi_pw_file.name,
775 "--dest-instance-name=%s" % di,
776 "--dest-primary-node=%s" % pn,
777 "--dest-secondary-node=%s" % sn,
778 "--net=0:mac=%s" % constants.VALUE_GENERATE,
784 qa_utils.RunInstanceCheck(di, False)
785 AssertEqual(StartLocalCommand(cmd).wait(), 0)
786 qa_utils.RunInstanceCheck(si, False)
787 qa_utils.RunInstanceCheck(di, True)