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 AssertMatch(data, r"^\d+$")
155 """Testing remote API version.
159 ("/version", constants.RAPI_VERSION, "GET", None),
163 def TestEmptyCluster():
164 """Testing remote API on an empty cluster.
167 master = qa_config.GetMasterNode()
168 master_full = qa_utils.ResolveNodeName(master)
170 def _VerifyInfo(data):
171 AssertIn("name", data)
172 AssertIn("master", data)
173 AssertEqual(data["master"], master_full)
175 def _VerifyNodes(data):
178 "uri": "/2/nodes/%s" % master_full,
180 AssertIn(master_entry, data)
182 def _VerifyNodesBulk(data):
184 for entry in NODE_FIELDS:
185 AssertIn(entry, node)
187 def _VerifyGroups(data):
189 "name": constants.INITIAL_NODE_GROUP_NAME,
190 "uri": "/2/groups/" + constants.INITIAL_NODE_GROUP_NAME,
192 AssertIn(default_group, data)
194 def _VerifyGroupsBulk(data):
196 for field in GROUP_FIELDS:
197 AssertIn(field, group)
200 ("/", None, "GET", None),
201 ("/2/info", _VerifyInfo, "GET", None),
202 ("/2/tags", None, "GET", None),
203 ("/2/nodes", _VerifyNodes, "GET", None),
204 ("/2/nodes?bulk=1", _VerifyNodesBulk, "GET", None),
205 ("/2/groups", _VerifyGroups, "GET", None),
206 ("/2/groups?bulk=1", _VerifyGroupsBulk, "GET", None),
207 ("/2/instances", [], "GET", None),
208 ("/2/instances?bulk=1", [], "GET", None),
209 ("/2/os", None, "GET", None),
212 # Test HTTP Not Found
213 for method in ["GET", "PUT", "POST", "DELETE"]:
215 _DoTests([("/99/resource/not/here/99", None, method, None)])
216 except rapi.client.GanetiApiError, err:
217 AssertEqual(err.code, 404)
219 raise qa_error.Error("Non-existent resource didn't return HTTP 404")
221 # Test HTTP Not Implemented
222 for method in ["PUT", "POST", "DELETE"]:
224 _DoTests([("/version", None, method, None)])
225 except rapi.client.GanetiApiError, err:
226 AssertEqual(err.code, 501)
228 raise qa_error.Error("Non-implemented method didn't fail")
232 """Testing resource queries via remote API.
235 master_name = qa_utils.ResolveNodeName(qa_config.GetMasterNode())
236 rnd = random.Random(7818)
238 for what in constants.QR_VIA_RAPI:
239 if what == constants.QR_JOB:
241 elif what == constants.QR_EXPORT:
246 all_fields = query.ALL_FIELDS[what].keys()
247 rnd.shuffle(all_fields)
249 # No fields, should return everything
250 result = _rapi_client.QueryFields(what)
251 qresult = objects.QueryFieldsResponse.FromDict(result)
252 AssertEqual(len(qresult.fields), len(all_fields))
255 result = _rapi_client.QueryFields(what, fields=[namefield])
256 qresult = objects.QueryFieldsResponse.FromDict(result)
257 AssertEqual(len(qresult.fields), 1)
259 # Specify all fields, order must be correct
260 result = _rapi_client.QueryFields(what, fields=all_fields)
261 qresult = objects.QueryFieldsResponse.FromDict(result)
262 AssertEqual(len(qresult.fields), len(all_fields))
263 AssertEqual([fdef.name for fdef in qresult.fields], all_fields)
266 result = _rapi_client.QueryFields(what, fields=["_unknown!"])
267 qresult = objects.QueryFieldsResponse.FromDict(result)
268 AssertEqual(len(qresult.fields), 1)
269 AssertEqual(qresult.fields[0].name, "_unknown!")
270 AssertEqual(qresult.fields[0].kind, constants.QFT_UNKNOWN)
272 # Try once more, this time without the client
274 ("/2/query/%s/fields" % what, None, "GET", None),
275 ("/2/query/%s/fields?fields=name,name,%s" % (what, all_fields[0]),
279 # Try missing query argument
282 ("/2/query/%s" % what, None, "GET", None),
284 except rapi.client.GanetiApiError, err:
285 AssertEqual(err.code, 400)
287 raise qa_error.Error("Request missing 'fields' parameter didn't fail")
289 def _Check(exp_fields, data):
290 qresult = objects.QueryResponse.FromDict(data)
291 AssertEqual([fdef.name for fdef in qresult.fields], exp_fields)
292 if not isinstance(qresult.data, list):
293 raise qa_error.Error("Query did not return a list")
296 # Specify fields in query
297 ("/2/query/%s?fields=%s" % (what, ",".join(all_fields)),
298 compat.partial(_Check, all_fields), "GET", None),
300 ("/2/query/%s?fields=%s" % (what, namefield),
301 compat.partial(_Check, [namefield]), "GET", None),
304 ("/2/query/%s?fields=%s,%%20%s%%09,%s%%20" %
305 (what, namefield, namefield, namefield),
306 compat.partial(_Check, [namefield] * 3), "GET", None),
308 # PUT with fields in query
309 ("/2/query/%s?fields=%s" % (what, namefield),
310 compat.partial(_Check, [namefield]), "PUT", {}),
313 ("/2/query/%s" % what, compat.partial(_Check, all_fields), "PUT", {
314 "fields": all_fields,
317 ("/2/query/%s" % what, compat.partial(_Check, [namefield] * 4), "PUT", {
318 "fields": [namefield] * 4,
325 ("/2/query/%s" % what, compat.partial(_Check, all_fields), "PUT", {
326 "fields": all_fields,
327 "filter": [qlang.OP_TRUE, namefield],
331 if what == constants.QR_LOCK:
332 # Locks can't be filtered
335 except rapi.client.GanetiApiError, err:
336 AssertEqual(err.code, 500)
338 raise qa_error.Error("Filtering locks didn't fail")
342 if what == constants.QR_NODE:
344 (nodes, ) = _DoTests([("/2/query/%s" % what,
345 compat.partial(_Check, ["name", "master"]), "PUT", {
346 "fields": ["name", "master"],
347 "filter": [qlang.OP_TRUE, "master"],
349 qresult = objects.QueryResponse.FromDict(nodes)
350 AssertEqual(qresult.data, [
351 [[constants.RS_NORMAL, master_name], [constants.RS_NORMAL, True]],
355 @InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
356 def TestInstance(instance):
357 """Testing getting instance(s) info via remote API.
360 def _VerifyInstance(data):
361 for entry in INSTANCE_FIELDS:
362 AssertIn(entry, data)
364 def _VerifyInstancesList(data):
365 for instance in data:
366 for entry in LIST_FIELDS:
367 AssertIn(entry, instance)
369 def _VerifyInstancesBulk(data):
370 for instance_data in data:
371 _VerifyInstance(instance_data)
374 ("/2/instances/%s" % instance["name"], _VerifyInstance, "GET", None),
375 ("/2/instances", _VerifyInstancesList, "GET", None),
376 ("/2/instances?bulk=1", _VerifyInstancesBulk, "GET", None),
377 ("/2/instances/%s/activate-disks" % instance["name"],
378 _VerifyReturnsJob, "PUT", None),
379 ("/2/instances/%s/deactivate-disks" % instance["name"],
380 _VerifyReturnsJob, "PUT", None),
383 # Test OpBackupPrepare
384 (job_id, ) = _DoTests([
385 ("/2/instances/%s/prepare-export?mode=%s" %
386 (instance["name"], constants.EXPORT_MODE_REMOTE),
387 _VerifyReturnsJob, "PUT", None),
390 result = _WaitForRapiJob(job_id)[0]
391 AssertEqual(len(result["handshake"]), 3)
392 AssertEqual(result["handshake"][0], constants.RIE_VERSION)
393 AssertEqual(len(result["x509_key_name"]), 3)
394 AssertIn("-----BEGIN CERTIFICATE-----", result["x509_ca"])
398 """Testing getting node(s) info via remote API.
401 def _VerifyNode(data):
402 for entry in NODE_FIELDS:
403 AssertIn(entry, data)
405 def _VerifyNodesList(data):
407 for entry in LIST_FIELDS:
408 AssertIn(entry, node)
410 def _VerifyNodesBulk(data):
411 for node_data in data:
412 _VerifyNode(node_data)
415 ("/2/nodes/%s" % node["primary"], _VerifyNode, "GET", None),
416 ("/2/nodes", _VerifyNodesList, "GET", None),
417 ("/2/nodes?bulk=1", _VerifyNodesBulk, "GET", None),
421 def _FilterTags(seq):
422 """Removes unwanted tags from a sequence.
425 ignore_re = qa_config.get("ignore-tags-re", None)
428 return itertools.ifilterfalse(re.compile(ignore_re).match, seq)
433 def TestTags(kind, name, tags):
434 """Tests .../tags resources.
437 if kind == constants.TAG_CLUSTER:
439 elif kind == constants.TAG_NODE:
440 uri = "/2/nodes/%s/tags" % name
441 elif kind == constants.TAG_INSTANCE:
442 uri = "/2/instances/%s/tags" % name
443 elif kind == constants.TAG_NODEGROUP:
444 uri = "/2/groups/%s/tags" % name
446 raise errors.ProgrammerError("Unknown tag kind")
448 def _VerifyTags(data):
449 AssertEqual(sorted(tags), sorted(_FilterTags(data)))
451 queryargs = "&".join("tag=%s" % i for i in tags)
454 (job_id, ) = _DoTests([
455 ("%s?%s" % (uri, queryargs), _VerifyReturnsJob, "PUT", None),
457 _WaitForRapiJob(job_id)
461 (uri, _VerifyTags, "GET", None),
465 (job_id, ) = _DoTests([
466 ("%s?%s" % (uri, queryargs), _VerifyReturnsJob, "DELETE", None),
468 _WaitForRapiJob(job_id)
471 def _WaitForRapiJob(job_id):
472 """Waits for a job to finish.
475 def _VerifyJob(data):
476 AssertEqual(data["id"], job_id)
477 for field in JOB_FIELDS:
478 AssertIn(field, data)
481 ("/2/jobs/%s" % job_id, _VerifyJob, "GET", None),
484 return rapi.client_utils.PollJob(_rapi_client, job_id,
485 cli.StdioJobPollReportCb())
488 def TestRapiNodeGroups():
489 """Test several node group operations using RAPI.
492 groups = qa_config.get("groups", {})
493 group1, group2, group3 = groups.get("inexistent-groups",
494 ["group1", "group2", "group3"])[:3]
496 # Create a group with no attributes
501 (job_id, ) = _DoTests([
502 ("/2/groups", _VerifyReturnsJob, "POST", body),
505 _WaitForRapiJob(job_id)
507 # Create a group specifying alloc_policy
510 "alloc_policy": constants.ALLOC_POLICY_UNALLOCABLE,
513 (job_id, ) = _DoTests([
514 ("/2/groups", _VerifyReturnsJob, "POST", body),
517 _WaitForRapiJob(job_id)
519 # Modify alloc_policy
521 "alloc_policy": constants.ALLOC_POLICY_UNALLOCABLE,
524 (job_id, ) = _DoTests([
525 ("/2/groups/%s/modify" % group1, _VerifyReturnsJob, "PUT", body),
528 _WaitForRapiJob(job_id)
535 (job_id, ) = _DoTests([
536 ("/2/groups/%s/rename" % group2, _VerifyReturnsJob, "PUT", body),
539 _WaitForRapiJob(job_id)
542 for group in [group1, group3]:
543 (job_id, ) = _DoTests([
544 ("/2/groups/%s" % group, _VerifyReturnsJob, "DELETE", None),
547 _WaitForRapiJob(job_id)
550 def TestRapiInstanceAdd(node, use_client):
551 """Test adding a new instance via RAPI"""
552 instance = qa_config.AcquireInstance()
554 disk_sizes = [utils.ParseUnit(size) for size in qa_config.get("disk")]
555 disks = [{"size": size} for size in disk_sizes]
556 nic0_mac = qa_config.GetInstanceNicMac(instance,
557 default=constants.VALUE_GENERATE)
559 constants.INIC_MAC: nic0_mac,
563 constants.BE_MAXMEM: utils.ParseUnit(qa_config.get(constants.BE_MAXMEM)),
564 constants.BE_MINMEM: utils.ParseUnit(qa_config.get(constants.BE_MINMEM)),
568 job_id = _rapi_client.CreateInstance(constants.INSTANCE_CREATE,
572 os=qa_config.get("os"),
573 pnode=node["primary"],
578 "mode": constants.INSTANCE_CREATE,
579 "name": instance["name"],
580 "os_type": qa_config.get("os"),
581 "disk_template": constants.DT_PLAIN,
582 "pnode": node["primary"],
583 "beparams": beparams,
588 (job_id, ) = _DoTests([
589 ("/2/instances", _VerifyReturnsJob, "POST", body),
592 _WaitForRapiJob(job_id)
596 qa_config.ReleaseInstance(instance)
600 @InstanceCheck(None, INST_DOWN, FIRST_ARG)
601 def TestRapiInstanceRemove(instance, use_client):
602 """Test removing instance via RAPI"""
604 job_id = _rapi_client.DeleteInstance(instance["name"])
606 (job_id, ) = _DoTests([
607 ("/2/instances/%s" % instance["name"], _VerifyReturnsJob, "DELETE", None),
610 _WaitForRapiJob(job_id)
612 qa_config.ReleaseInstance(instance)
615 @InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
616 def TestRapiInstanceMigrate(instance):
617 """Test migrating instance via RAPI"""
618 # Move to secondary node
619 _WaitForRapiJob(_rapi_client.MigrateInstance(instance["name"]))
620 qa_utils.RunInstanceCheck(instance, True)
621 # And back to previous primary
622 _WaitForRapiJob(_rapi_client.MigrateInstance(instance["name"]))
625 @InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
626 def TestRapiInstanceFailover(instance):
627 """Test failing over instance via RAPI"""
628 # Move to secondary node
629 _WaitForRapiJob(_rapi_client.FailoverInstance(instance["name"]))
630 qa_utils.RunInstanceCheck(instance, True)
631 # And back to previous primary
632 _WaitForRapiJob(_rapi_client.FailoverInstance(instance["name"]))
635 @InstanceCheck(INST_UP, INST_DOWN, FIRST_ARG)
636 def TestRapiInstanceShutdown(instance):
637 """Test stopping an instance via RAPI"""
638 _WaitForRapiJob(_rapi_client.ShutdownInstance(instance["name"]))
641 @InstanceCheck(INST_DOWN, INST_UP, FIRST_ARG)
642 def TestRapiInstanceStartup(instance):
643 """Test starting an instance via RAPI"""
644 _WaitForRapiJob(_rapi_client.StartupInstance(instance["name"]))
647 @InstanceCheck(INST_DOWN, INST_DOWN, FIRST_ARG)
648 def TestRapiInstanceRenameAndBack(rename_source, rename_target):
649 """Test renaming instance via RAPI
651 This must leave the instance with the original name (in the
655 _WaitForRapiJob(_rapi_client.RenameInstance(rename_source, rename_target))
656 qa_utils.RunInstanceCheck(rename_source, False)
657 qa_utils.RunInstanceCheck(rename_target, False)
658 _WaitForRapiJob(_rapi_client.RenameInstance(rename_target, rename_source))
659 qa_utils.RunInstanceCheck(rename_target, False)
662 @InstanceCheck(INST_DOWN, INST_DOWN, FIRST_ARG)
663 def TestRapiInstanceReinstall(instance):
664 """Test reinstalling an instance via RAPI"""
665 _WaitForRapiJob(_rapi_client.ReinstallInstance(instance["name"]))
666 # By default, the instance is started again
667 qa_utils.RunInstanceCheck(instance, True)
669 # Reinstall again without starting
670 _WaitForRapiJob(_rapi_client.ReinstallInstance(instance["name"],
674 @InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
675 def TestRapiInstanceReplaceDisks(instance):
676 """Test replacing instance disks via RAPI"""
677 _WaitForRapiJob(_rapi_client.ReplaceInstanceDisks(instance["name"],
678 mode=constants.REPLACE_DISK_AUTO, disks=[]))
679 _WaitForRapiJob(_rapi_client.ReplaceInstanceDisks(instance["name"],
680 mode=constants.REPLACE_DISK_SEC, disks="0"))
683 @InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
684 def TestRapiInstanceModify(instance):
685 """Test modifying instance via RAPI"""
686 default_hv = qa_config.GetDefaultHypervisor()
688 def _ModifyInstance(**kwargs):
689 _WaitForRapiJob(_rapi_client.ModifyInstance(instance["name"], **kwargs))
691 _ModifyInstance(beparams={
692 constants.BE_VCPUS: 3,
695 _ModifyInstance(beparams={
696 constants.BE_VCPUS: constants.VALUE_DEFAULT,
699 if default_hv == constants.HT_XEN_PVM:
700 _ModifyInstance(hvparams={
701 constants.HV_KERNEL_ARGS: "single",
703 _ModifyInstance(hvparams={
704 constants.HV_KERNEL_ARGS: constants.VALUE_DEFAULT,
706 elif default_hv == constants.HT_XEN_HVM:
707 _ModifyInstance(hvparams={
708 constants.HV_BOOT_ORDER: "acn",
710 _ModifyInstance(hvparams={
711 constants.HV_BOOT_ORDER: constants.VALUE_DEFAULT,
715 @InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
716 def TestRapiInstanceConsole(instance):
717 """Test getting instance console information via RAPI"""
718 result = _rapi_client.GetInstanceConsole(instance["name"])
719 console = objects.InstanceConsole.FromDict(result)
720 AssertEqual(console.Validate(), True)
721 AssertEqual(console.instance, qa_utils.ResolveInstanceName(instance["name"]))
724 @InstanceCheck(INST_DOWN, INST_DOWN, FIRST_ARG)
725 def TestRapiStoppedInstanceConsole(instance):
726 """Test getting stopped instance's console information via RAPI"""
728 _rapi_client.GetInstanceConsole(instance["name"])
729 except rapi.client.GanetiApiError, err:
730 AssertEqual(err.code, 503)
732 raise qa_error.Error("Getting console for stopped instance didn't"
736 def GetOperatingSystems():
737 """Retrieves a list of all available operating systems.
740 return _rapi_client.GetOperatingSystems()
743 def TestInterClusterInstanceMove(src_instance, dest_instance,
744 pnode, snode, tnode):
745 """Test tools/move-instance"""
746 master = qa_config.GetMasterNode()
748 rapi_pw_file = tempfile.NamedTemporaryFile()
749 rapi_pw_file.write(_rapi_password)
752 # TODO: Run some instance tests before moving back
755 # instance is not redundant, but we still need to pass a node
756 # (which will be ignored)
760 # note: pnode:snode are the *current* nodes, so we move it first to
761 # tnode:pnode, then back to pnode:snode
762 for si, di, pn, sn in [(src_instance["name"], dest_instance["name"],
763 tnode["primary"], pnode["primary"]),
764 (dest_instance["name"], src_instance["name"],
765 pnode["primary"], fsec["primary"])]:
767 "../tools/move-instance",
769 "--src-ca-file=%s" % _rapi_ca.name,
770 "--src-username=%s" % _rapi_username,
771 "--src-password-file=%s" % rapi_pw_file.name,
772 "--dest-instance-name=%s" % di,
773 "--dest-primary-node=%s" % pn,
774 "--dest-secondary-node=%s" % sn,
775 "--net=0:mac=%s" % constants.VALUE_GENERATE,
781 qa_utils.RunInstanceCheck(di, False)
782 AssertEqual(StartLocalCommand(cmd).wait(), 0)
783 qa_utils.RunInstanceCheck(si, False)
784 qa_utils.RunInstanceCheck(di, True)