3 # Copyright (C) 2007, 2008, 2009, 2010, 2011 Google Inc.
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 2 of the License, or
8 # (at your option) any later version.
10 # This program is distributed in the hope that it will be useful, but
11 # WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 # General Public License for more details.
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software
17 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
21 """Remote API QA tests.
28 from ganeti import utils
29 from ganeti import constants
30 from ganeti import errors
31 from ganeti import cli
32 from ganeti import rapi
33 from ganeti import objects
34 from ganeti import query
35 from ganeti import compat
36 from ganeti import qlang
38 import ganeti.rapi.client # pylint: disable=W0611
39 import ganeti.rapi.client_utils
45 from qa_utils import (AssertEqual, AssertIn, AssertMatch, StartLocalCommand)
54 def Setup(username, password):
55 """Configures the RAPI client.
58 # pylint: disable=W0603
65 _rapi_username = username
66 _rapi_password = password
68 master = qa_config.GetMasterNode()
70 # Load RAPI certificate from master node
71 cmd = ["cat", constants.RAPI_CERT_FILE]
73 # Write to temporary file
74 _rapi_ca = tempfile.NamedTemporaryFile()
75 _rapi_ca.write(qa_utils.GetCommandOutput(master["primary"],
76 utils.ShellQuoteArgs(cmd)))
79 port = qa_config.get("rapi-port", default=constants.DEFAULT_RAPI_PORT)
80 cfg_curl = rapi.client.GenericCurlConfig(cafile=_rapi_ca.name,
83 _rapi_client = rapi.client.GanetiRapiClient(master["primary"], port=port,
86 curl_config_fn=cfg_curl)
88 print "RAPI protocol version: %s" % _rapi_client.GetVersion()
91 INSTANCE_FIELDS = ("name", "os", "pnode", "snodes",
93 "disk_template", "disk.sizes",
94 "nic.ips", "nic.macs", "nic.modes", "nic.links",
95 "beparams", "hvparams",
96 "oper_state", "oper_ram", "oper_vcpus", "status", "tags")
98 NODE_FIELDS = ("name", "dtotal", "dfree",
99 "mtotal", "mnode", "mfree",
100 "pinst_cnt", "sinst_cnt", "tags")
102 GROUP_FIELDS = frozenset([
105 "node_cnt", "node_list",
108 JOB_FIELDS = frozenset([
109 "id", "ops", "status", "summary",
110 "opstatus", "opresult", "oplog",
111 "received_ts", "start_ts", "end_ts",
114 LIST_FIELDS = ("id", "uri")
118 """Return whether remote API tests should be run.
121 return qa_config.TestEnabled("rapi")
125 # pylint: disable=W0212
126 # due to _SendRequest usage
129 for uri, verify, method, body in uris:
130 assert uri.startswith("/")
132 print "%s %s" % (method, uri)
133 data = _rapi_client._SendRequest(method, uri, None, body)
135 if verify is not None:
139 AssertEqual(data, verify)
146 def _VerifyReturnsJob(data):
147 AssertMatch(data, r"^\d+$")
151 """Testing remote API version.
155 ("/version", constants.RAPI_VERSION, "GET", None),
159 def TestEmptyCluster():
160 """Testing remote API on an empty cluster.
163 master = qa_config.GetMasterNode()
164 master_full = qa_utils.ResolveNodeName(master)
166 def _VerifyInfo(data):
167 AssertIn("name", data)
168 AssertIn("master", data)
169 AssertEqual(data["master"], master_full)
171 def _VerifyNodes(data):
174 "uri": "/2/nodes/%s" % master_full,
176 AssertIn(master_entry, data)
178 def _VerifyNodesBulk(data):
180 for entry in NODE_FIELDS:
181 AssertIn(entry, node)
183 def _VerifyGroups(data):
185 "name": constants.INITIAL_NODE_GROUP_NAME,
186 "uri": "/2/groups/" + constants.INITIAL_NODE_GROUP_NAME,
188 AssertIn(default_group, data)
190 def _VerifyGroupsBulk(data):
192 for field in GROUP_FIELDS:
193 AssertIn(field, group)
196 ("/", None, "GET", None),
197 ("/2/info", _VerifyInfo, "GET", None),
198 ("/2/tags", None, "GET", None),
199 ("/2/nodes", _VerifyNodes, "GET", None),
200 ("/2/nodes?bulk=1", _VerifyNodesBulk, "GET", None),
201 ("/2/groups", _VerifyGroups, "GET", None),
202 ("/2/groups?bulk=1", _VerifyGroupsBulk, "GET", None),
203 ("/2/instances", [], "GET", None),
204 ("/2/instances?bulk=1", [], "GET", None),
205 ("/2/os", None, "GET", None),
208 # Test HTTP Not Found
209 for method in ["GET", "PUT", "POST", "DELETE"]:
211 _DoTests([("/99/resource/not/here/99", None, method, None)])
212 except rapi.client.GanetiApiError, err:
213 AssertEqual(err.code, 404)
215 raise qa_error.Error("Non-existent resource didn't return HTTP 404")
217 # Test HTTP Not Implemented
218 for method in ["PUT", "POST", "DELETE"]:
220 _DoTests([("/version", None, method, None)])
221 except rapi.client.GanetiApiError, err:
222 AssertEqual(err.code, 501)
224 raise qa_error.Error("Non-implemented method didn't fail")
228 """Testing resource queries via remote API.
231 master_name = qa_utils.ResolveNodeName(qa_config.GetMasterNode())
232 rnd = random.Random(7818)
234 for what in constants.QR_VIA_RAPI:
235 all_fields = query.ALL_FIELDS[what].keys()
236 rnd.shuffle(all_fields)
238 # No fields, should return everything
239 result = _rapi_client.QueryFields(what)
240 qresult = objects.QueryFieldsResponse.FromDict(result)
241 AssertEqual(len(qresult.fields), len(all_fields))
244 result = _rapi_client.QueryFields(what, fields=["name"])
245 qresult = objects.QueryFieldsResponse.FromDict(result)
246 AssertEqual(len(qresult.fields), 1)
248 # Specify all fields, order must be correct
249 result = _rapi_client.QueryFields(what, fields=all_fields)
250 qresult = objects.QueryFieldsResponse.FromDict(result)
251 AssertEqual(len(qresult.fields), len(all_fields))
252 AssertEqual([fdef.name for fdef in qresult.fields], all_fields)
255 result = _rapi_client.QueryFields(what, fields=["_unknown!"])
256 qresult = objects.QueryFieldsResponse.FromDict(result)
257 AssertEqual(len(qresult.fields), 1)
258 AssertEqual(qresult.fields[0].name, "_unknown!")
259 AssertEqual(qresult.fields[0].kind, constants.QFT_UNKNOWN)
261 # Try once more, this time without the client
263 ("/2/query/%s/fields" % what, None, "GET", None),
264 ("/2/query/%s/fields?fields=name,name,%s" % (what, all_fields[0]),
268 # Try missing query argument
271 ("/2/query/%s" % what, None, "GET", None),
273 except rapi.client.GanetiApiError, err:
274 AssertEqual(err.code, 400)
276 raise qa_error.Error("Request missing 'fields' parameter didn't fail")
278 def _Check(exp_fields, data):
279 qresult = objects.QueryResponse.FromDict(data)
280 AssertEqual([fdef.name for fdef in qresult.fields], exp_fields)
281 if not isinstance(qresult.data, list):
282 raise qa_error.Error("Query did not return a list")
285 # Specify fields in query
286 ("/2/query/%s?fields=%s" % (what, ",".join(all_fields)),
287 compat.partial(_Check, all_fields), "GET", None),
289 ("/2/query/%s?fields=name" % what,
290 compat.partial(_Check, ["name"]), "GET", None),
293 ("/2/query/%s?fields=name,%%20name%%09,name%%20" % what,
294 compat.partial(_Check, ["name"] * 3), "GET", None),
296 # PUT with fields in query
297 ("/2/query/%s?fields=name" % what,
298 compat.partial(_Check, ["name"]), "PUT", {}),
301 ("/2/query/%s" % what, compat.partial(_Check, all_fields), "PUT", {
302 "fields": all_fields,
305 ("/2/query/%s" % what, compat.partial(_Check, ["name"] * 4), "PUT", {
306 "fields": ["name"] * 4,
313 ("/2/query/%s" % what, compat.partial(_Check, all_fields), "PUT", {
314 "fields": all_fields,
315 "filter": [qlang.OP_TRUE, "name"],
319 if what == constants.QR_LOCK:
320 # Locks can't be filtered
323 except rapi.client.GanetiApiError, err:
324 AssertEqual(err.code, 500)
326 raise qa_error.Error("Filtering locks didn't fail")
330 if what == constants.QR_NODE:
332 (nodes, ) = _DoTests([("/2/query/%s" % what,
333 compat.partial(_Check, ["name", "master"]), "PUT", {
334 "fields": ["name", "master"],
335 "filter": [qlang.OP_TRUE, "master"],
337 qresult = objects.QueryResponse.FromDict(nodes)
338 AssertEqual(qresult.data, [
339 [[constants.RS_NORMAL, master_name], [constants.RS_NORMAL, True]],
343 def TestInstance(instance):
344 """Testing getting instance(s) info via remote API.
347 def _VerifyInstance(data):
348 for entry in INSTANCE_FIELDS:
349 AssertIn(entry, data)
351 def _VerifyInstancesList(data):
352 for instance in data:
353 for entry in LIST_FIELDS:
354 AssertIn(entry, instance)
356 def _VerifyInstancesBulk(data):
357 for instance_data in data:
358 _VerifyInstance(instance_data)
361 ("/2/instances/%s" % instance["name"], _VerifyInstance, "GET", None),
362 ("/2/instances", _VerifyInstancesList, "GET", None),
363 ("/2/instances?bulk=1", _VerifyInstancesBulk, "GET", None),
364 ("/2/instances/%s/activate-disks" % instance["name"],
365 _VerifyReturnsJob, "PUT", None),
366 ("/2/instances/%s/deactivate-disks" % instance["name"],
367 _VerifyReturnsJob, "PUT", None),
370 # Test OpBackupPrepare
371 (job_id, ) = _DoTests([
372 ("/2/instances/%s/prepare-export?mode=%s" %
373 (instance["name"], constants.EXPORT_MODE_REMOTE),
374 _VerifyReturnsJob, "PUT", None),
377 result = _WaitForRapiJob(job_id)[0]
378 AssertEqual(len(result["handshake"]), 3)
379 AssertEqual(result["handshake"][0], constants.RIE_VERSION)
380 AssertEqual(len(result["x509_key_name"]), 3)
381 AssertIn("-----BEGIN CERTIFICATE-----", result["x509_ca"])
385 """Testing getting node(s) info via remote API.
388 def _VerifyNode(data):
389 for entry in NODE_FIELDS:
390 AssertIn(entry, data)
392 def _VerifyNodesList(data):
394 for entry in LIST_FIELDS:
395 AssertIn(entry, node)
397 def _VerifyNodesBulk(data):
398 for node_data in data:
399 _VerifyNode(node_data)
402 ("/2/nodes/%s" % node["primary"], _VerifyNode, "GET", None),
403 ("/2/nodes", _VerifyNodesList, "GET", None),
404 ("/2/nodes?bulk=1", _VerifyNodesBulk, "GET", None),
408 def TestTags(kind, name, tags):
409 """Tests .../tags resources.
412 if kind == constants.TAG_CLUSTER:
414 elif kind == constants.TAG_NODE:
415 uri = "/2/nodes/%s/tags" % name
416 elif kind == constants.TAG_INSTANCE:
417 uri = "/2/instances/%s/tags" % name
418 elif kind == constants.TAG_NODEGROUP:
419 uri = "/2/groups/%s/tags" % name
421 raise errors.ProgrammerError("Unknown tag kind")
423 def _VerifyTags(data):
424 AssertEqual(sorted(tags), sorted(data))
426 queryargs = "&".join("tag=%s" % i for i in tags)
429 (job_id, ) = _DoTests([
430 ("%s?%s" % (uri, queryargs), _VerifyReturnsJob, "PUT", None),
432 _WaitForRapiJob(job_id)
436 (uri, _VerifyTags, "GET", None),
440 (job_id, ) = _DoTests([
441 ("%s?%s" % (uri, queryargs), _VerifyReturnsJob, "DELETE", None),
443 _WaitForRapiJob(job_id)
446 def _WaitForRapiJob(job_id):
447 """Waits for a job to finish.
450 def _VerifyJob(data):
451 AssertEqual(data["id"], job_id)
452 for field in JOB_FIELDS:
453 AssertIn(field, data)
456 ("/2/jobs/%s" % job_id, _VerifyJob, "GET", None),
459 return rapi.client_utils.PollJob(_rapi_client, job_id,
460 cli.StdioJobPollReportCb())
463 def TestRapiNodeGroups():
464 """Test several node group operations using RAPI.
467 groups = qa_config.get("groups", {})
468 group1, group2, group3 = groups.get("inexistent-groups",
469 ["group1", "group2", "group3"])[:3]
471 # Create a group with no attributes
476 (job_id, ) = _DoTests([
477 ("/2/groups", _VerifyReturnsJob, "POST", body),
480 _WaitForRapiJob(job_id)
482 # Create a group specifying alloc_policy
485 "alloc_policy": constants.ALLOC_POLICY_UNALLOCABLE,
488 (job_id, ) = _DoTests([
489 ("/2/groups", _VerifyReturnsJob, "POST", body),
492 _WaitForRapiJob(job_id)
494 # Modify alloc_policy
496 "alloc_policy": constants.ALLOC_POLICY_UNALLOCABLE,
499 (job_id, ) = _DoTests([
500 ("/2/groups/%s/modify" % group1, _VerifyReturnsJob, "PUT", body),
503 _WaitForRapiJob(job_id)
510 (job_id, ) = _DoTests([
511 ("/2/groups/%s/rename" % group2, _VerifyReturnsJob, "PUT", body),
514 _WaitForRapiJob(job_id)
517 for group in [group1, group3]:
518 (job_id, ) = _DoTests([
519 ("/2/groups/%s" % group, _VerifyReturnsJob, "DELETE", None),
522 _WaitForRapiJob(job_id)
525 def TestRapiInstanceAdd(node, use_client):
526 """Test adding a new instance via RAPI"""
527 instance = qa_config.AcquireInstance()
529 memory = utils.ParseUnit(qa_config.get("mem"))
530 disk_sizes = [utils.ParseUnit(size) for size in qa_config.get("disk")]
531 disks = [{"size": size} for size in disk_sizes]
535 constants.BE_MEMORY: memory,
539 job_id = _rapi_client.CreateInstance(constants.INSTANCE_CREATE,
543 os=qa_config.get("os"),
544 pnode=node["primary"],
549 "mode": constants.INSTANCE_CREATE,
550 "name": instance["name"],
551 "os_type": qa_config.get("os"),
552 "disk_template": constants.DT_PLAIN,
553 "pnode": node["primary"],
554 "beparams": beparams,
559 (job_id, ) = _DoTests([
560 ("/2/instances", _VerifyReturnsJob, "POST", body),
563 _WaitForRapiJob(job_id)
567 qa_config.ReleaseInstance(instance)
571 def TestRapiInstanceRemove(instance, use_client):
572 """Test removing instance via RAPI"""
574 job_id = _rapi_client.DeleteInstance(instance["name"])
576 (job_id, ) = _DoTests([
577 ("/2/instances/%s" % instance["name"], _VerifyReturnsJob, "DELETE", None),
580 _WaitForRapiJob(job_id)
582 qa_config.ReleaseInstance(instance)
585 def TestRapiInstanceMigrate(instance):
586 """Test migrating instance via RAPI"""
587 # Move to secondary node
588 _WaitForRapiJob(_rapi_client.MigrateInstance(instance["name"]))
589 # And back to previous primary
590 _WaitForRapiJob(_rapi_client.MigrateInstance(instance["name"]))
593 def TestRapiInstanceFailover(instance):
594 """Test failing over instance via RAPI"""
595 # Move to secondary node
596 _WaitForRapiJob(_rapi_client.FailoverInstance(instance["name"]))
597 # And back to previous primary
598 _WaitForRapiJob(_rapi_client.FailoverInstance(instance["name"]))
601 def TestRapiInstanceShutdown(instance):
602 """Test stopping an instance via RAPI"""
603 _WaitForRapiJob(_rapi_client.ShutdownInstance(instance["name"]))
606 def TestRapiInstanceStartup(instance):
607 """Test starting an instance via RAPI"""
608 _WaitForRapiJob(_rapi_client.StartupInstance(instance["name"]))
611 def TestRapiInstanceRename(rename_source, rename_target):
612 """Test renaming instance via RAPI"""
613 _WaitForRapiJob(_rapi_client.RenameInstance(rename_source, rename_target))
616 def TestRapiInstanceReinstall(instance):
617 """Test reinstalling an instance via RAPI"""
618 _WaitForRapiJob(_rapi_client.ReinstallInstance(instance["name"]))
621 def TestRapiInstanceReplaceDisks(instance):
622 """Test replacing instance disks via RAPI"""
623 _WaitForRapiJob(_rapi_client.ReplaceInstanceDisks(instance["name"],
624 mode=constants.REPLACE_DISK_AUTO, disks=[]))
625 _WaitForRapiJob(_rapi_client.ReplaceInstanceDisks(instance["name"],
626 mode=constants.REPLACE_DISK_SEC, disks="0"))
629 def TestRapiInstanceModify(instance):
630 """Test modifying instance via RAPI"""
631 def _ModifyInstance(**kwargs):
632 _WaitForRapiJob(_rapi_client.ModifyInstance(instance["name"], **kwargs))
634 _ModifyInstance(hvparams={
635 constants.HV_KERNEL_ARGS: "single",
638 _ModifyInstance(beparams={
639 constants.BE_VCPUS: 3,
642 _ModifyInstance(beparams={
643 constants.BE_VCPUS: constants.VALUE_DEFAULT,
646 _ModifyInstance(hvparams={
647 constants.HV_KERNEL_ARGS: constants.VALUE_DEFAULT,
651 def TestRapiInstanceConsole(instance):
652 """Test getting instance console information via RAPI"""
653 result = _rapi_client.GetInstanceConsole(instance["name"])
654 console = objects.InstanceConsole.FromDict(result)
655 AssertEqual(console.Validate(), True)
656 AssertEqual(console.instance, qa_utils.ResolveInstanceName(instance["name"]))
659 def TestRapiStoppedInstanceConsole(instance):
660 """Test getting stopped instance's console information via RAPI"""
662 _rapi_client.GetInstanceConsole(instance["name"])
663 except rapi.client.GanetiApiError, err:
664 AssertEqual(err.code, 503)
666 raise qa_error.Error("Getting console for stopped instance didn't"
670 def GetOperatingSystems():
671 """Retrieves a list of all available operating systems.
674 return _rapi_client.GetOperatingSystems()
677 def TestInterClusterInstanceMove(src_instance, dest_instance,
678 pnode, snode, tnode):
679 """Test tools/move-instance"""
680 master = qa_config.GetMasterNode()
682 rapi_pw_file = tempfile.NamedTemporaryFile()
683 rapi_pw_file.write(_rapi_password)
686 # TODO: Run some instance tests before moving back
689 # instance is not redundant, but we still need to pass a node
690 # (which will be ignored)
694 # note: pnode:snode are the *current* nodes, so we move it first to
695 # tnode:pnode, then back to pnode:snode
696 for si, di, pn, sn in [(src_instance["name"], dest_instance["name"],
697 tnode["primary"], pnode["primary"]),
698 (dest_instance["name"], src_instance["name"],
699 pnode["primary"], fsec["primary"])]:
701 "../tools/move-instance",
703 "--src-ca-file=%s" % _rapi_ca.name,
704 "--src-username=%s" % _rapi_username,
705 "--src-password-file=%s" % rapi_pw_file.name,
706 "--dest-instance-name=%s" % di,
707 "--dest-primary-node=%s" % pn,
708 "--dest-secondary-node=%s" % sn,
709 "--net=0:mac=%s" % constants.VALUE_GENERATE,
715 AssertEqual(StartLocalCommand(cmd).wait(), 0)