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.
29 from ganeti import utils
30 from ganeti import constants
31 from ganeti import errors
32 from ganeti import cli
33 from ganeti import rapi
34 from ganeti import objects
35 from ganeti import query
36 from ganeti import compat
37 from ganeti import qlang
39 import ganeti.rapi.client # pylint: disable=W0611
40 import ganeti.rapi.client_utils
46 from qa_utils import (AssertEqual, AssertIn, AssertMatch, StartLocalCommand)
55 def Setup(username, password):
56 """Configures the RAPI client.
59 # pylint: disable=W0603
66 _rapi_username = username
67 _rapi_password = password
69 master = qa_config.GetMasterNode()
71 # Load RAPI certificate from master node
72 cmd = ["cat", constants.RAPI_CERT_FILE]
74 # Write to temporary file
75 _rapi_ca = tempfile.NamedTemporaryFile()
76 _rapi_ca.write(qa_utils.GetCommandOutput(master["primary"],
77 utils.ShellQuoteArgs(cmd)))
80 port = qa_config.get("rapi-port", default=constants.DEFAULT_RAPI_PORT)
81 cfg_curl = rapi.client.GenericCurlConfig(cafile=_rapi_ca.name,
84 _rapi_client = rapi.client.GanetiRapiClient(master["primary"], port=port,
87 curl_config_fn=cfg_curl)
89 print "RAPI protocol version: %s" % _rapi_client.GetVersion()
92 INSTANCE_FIELDS = ("name", "os", "pnode", "snodes",
94 "disk_template", "disk.sizes",
95 "nic.ips", "nic.macs", "nic.modes", "nic.links",
96 "beparams", "hvparams",
97 "oper_state", "oper_ram", "oper_vcpus", "status", "tags")
99 NODE_FIELDS = ("name", "dtotal", "dfree",
100 "mtotal", "mnode", "mfree",
101 "pinst_cnt", "sinst_cnt", "tags")
103 GROUP_FIELDS = frozenset([
106 "node_cnt", "node_list",
109 JOB_FIELDS = frozenset([
110 "id", "ops", "status", "summary",
111 "opstatus", "opresult", "oplog",
112 "received_ts", "start_ts", "end_ts",
115 LIST_FIELDS = ("id", "uri")
119 """Return whether remote API tests should be run.
122 return qa_config.TestEnabled("rapi")
126 # pylint: disable=W0212
127 # due to _SendRequest usage
130 for uri, verify, method, body in uris:
131 assert uri.startswith("/")
133 print "%s %s" % (method, uri)
134 data = _rapi_client._SendRequest(method, uri, None, body)
136 if verify is not None:
140 AssertEqual(data, verify)
147 def _VerifyReturnsJob(data):
148 AssertMatch(data, r"^\d+$")
152 """Testing remote API version.
156 ("/version", constants.RAPI_VERSION, "GET", None),
160 def TestEmptyCluster():
161 """Testing remote API on an empty cluster.
164 master = qa_config.GetMasterNode()
165 master_full = qa_utils.ResolveNodeName(master)
167 def _VerifyInfo(data):
168 AssertIn("name", data)
169 AssertIn("master", data)
170 AssertEqual(data["master"], master_full)
172 def _VerifyNodes(data):
175 "uri": "/2/nodes/%s" % master_full,
177 AssertIn(master_entry, data)
179 def _VerifyNodesBulk(data):
181 for entry in NODE_FIELDS:
182 AssertIn(entry, node)
184 def _VerifyGroups(data):
186 "name": constants.INITIAL_NODE_GROUP_NAME,
187 "uri": "/2/groups/" + constants.INITIAL_NODE_GROUP_NAME,
189 AssertIn(default_group, data)
191 def _VerifyGroupsBulk(data):
193 for field in GROUP_FIELDS:
194 AssertIn(field, group)
197 ("/", None, "GET", None),
198 ("/2/info", _VerifyInfo, "GET", None),
199 ("/2/tags", None, "GET", None),
200 ("/2/nodes", _VerifyNodes, "GET", None),
201 ("/2/nodes?bulk=1", _VerifyNodesBulk, "GET", None),
202 ("/2/groups", _VerifyGroups, "GET", None),
203 ("/2/groups?bulk=1", _VerifyGroupsBulk, "GET", None),
204 ("/2/instances", [], "GET", None),
205 ("/2/instances?bulk=1", [], "GET", None),
206 ("/2/os", None, "GET", None),
209 # Test HTTP Not Found
210 for method in ["GET", "PUT", "POST", "DELETE"]:
212 _DoTests([("/99/resource/not/here/99", None, method, None)])
213 except rapi.client.GanetiApiError, err:
214 AssertEqual(err.code, 404)
216 raise qa_error.Error("Non-existent resource didn't return HTTP 404")
218 # Test HTTP Not Implemented
219 for method in ["PUT", "POST", "DELETE"]:
221 _DoTests([("/version", None, method, None)])
222 except rapi.client.GanetiApiError, err:
223 AssertEqual(err.code, 501)
225 raise qa_error.Error("Non-implemented method didn't fail")
229 """Testing resource queries via remote API.
232 master_name = qa_utils.ResolveNodeName(qa_config.GetMasterNode())
233 rnd = random.Random(7818)
235 for what in constants.QR_VIA_RAPI:
236 all_fields = query.ALL_FIELDS[what].keys()
237 rnd.shuffle(all_fields)
239 # No fields, should return everything
240 result = _rapi_client.QueryFields(what)
241 qresult = objects.QueryFieldsResponse.FromDict(result)
242 AssertEqual(len(qresult.fields), len(all_fields))
245 result = _rapi_client.QueryFields(what, fields=["name"])
246 qresult = objects.QueryFieldsResponse.FromDict(result)
247 AssertEqual(len(qresult.fields), 1)
249 # Specify all fields, order must be correct
250 result = _rapi_client.QueryFields(what, fields=all_fields)
251 qresult = objects.QueryFieldsResponse.FromDict(result)
252 AssertEqual(len(qresult.fields), len(all_fields))
253 AssertEqual([fdef.name for fdef in qresult.fields], all_fields)
256 result = _rapi_client.QueryFields(what, fields=["_unknown!"])
257 qresult = objects.QueryFieldsResponse.FromDict(result)
258 AssertEqual(len(qresult.fields), 1)
259 AssertEqual(qresult.fields[0].name, "_unknown!")
260 AssertEqual(qresult.fields[0].kind, constants.QFT_UNKNOWN)
262 # Try once more, this time without the client
264 ("/2/query/%s/fields" % what, None, "GET", None),
265 ("/2/query/%s/fields?fields=name,name,%s" % (what, all_fields[0]),
269 # Try missing query argument
272 ("/2/query/%s" % what, None, "GET", None),
274 except rapi.client.GanetiApiError, err:
275 AssertEqual(err.code, 400)
277 raise qa_error.Error("Request missing 'fields' parameter didn't fail")
279 def _Check(exp_fields, data):
280 qresult = objects.QueryResponse.FromDict(data)
281 AssertEqual([fdef.name for fdef in qresult.fields], exp_fields)
282 if not isinstance(qresult.data, list):
283 raise qa_error.Error("Query did not return a list")
286 # Specify fields in query
287 ("/2/query/%s?fields=%s" % (what, ",".join(all_fields)),
288 compat.partial(_Check, all_fields), "GET", None),
290 ("/2/query/%s?fields=name" % what,
291 compat.partial(_Check, ["name"]), "GET", None),
294 ("/2/query/%s?fields=name,%%20name%%09,name%%20" % what,
295 compat.partial(_Check, ["name"] * 3), "GET", None),
297 # PUT with fields in query
298 ("/2/query/%s?fields=name" % what,
299 compat.partial(_Check, ["name"]), "PUT", {}),
302 ("/2/query/%s" % what, compat.partial(_Check, all_fields), "PUT", {
303 "fields": all_fields,
306 ("/2/query/%s" % what, compat.partial(_Check, ["name"] * 4), "PUT", {
307 "fields": ["name"] * 4,
314 ("/2/query/%s" % what, compat.partial(_Check, all_fields), "PUT", {
315 "fields": all_fields,
316 "filter": [qlang.OP_TRUE, "name"],
320 if what == constants.QR_LOCK:
321 # Locks can't be filtered
324 except rapi.client.GanetiApiError, err:
325 AssertEqual(err.code, 500)
327 raise qa_error.Error("Filtering locks didn't fail")
331 if what == constants.QR_NODE:
333 (nodes, ) = _DoTests([("/2/query/%s" % what,
334 compat.partial(_Check, ["name", "master"]), "PUT", {
335 "fields": ["name", "master"],
336 "filter": [qlang.OP_TRUE, "master"],
338 qresult = objects.QueryResponse.FromDict(nodes)
339 AssertEqual(qresult.data, [
340 [[constants.RS_NORMAL, master_name], [constants.RS_NORMAL, True]],
344 def TestInstance(instance):
345 """Testing getting instance(s) info via remote API.
348 def _VerifyInstance(data):
349 for entry in INSTANCE_FIELDS:
350 AssertIn(entry, data)
352 def _VerifyInstancesList(data):
353 for instance in data:
354 for entry in LIST_FIELDS:
355 AssertIn(entry, instance)
357 def _VerifyInstancesBulk(data):
358 for instance_data in data:
359 _VerifyInstance(instance_data)
362 ("/2/instances/%s" % instance["name"], _VerifyInstance, "GET", None),
363 ("/2/instances", _VerifyInstancesList, "GET", None),
364 ("/2/instances?bulk=1", _VerifyInstancesBulk, "GET", None),
365 ("/2/instances/%s/activate-disks" % instance["name"],
366 _VerifyReturnsJob, "PUT", None),
367 ("/2/instances/%s/deactivate-disks" % instance["name"],
368 _VerifyReturnsJob, "PUT", None),
371 # Test OpBackupPrepare
372 (job_id, ) = _DoTests([
373 ("/2/instances/%s/prepare-export?mode=%s" %
374 (instance["name"], constants.EXPORT_MODE_REMOTE),
375 _VerifyReturnsJob, "PUT", None),
378 result = _WaitForRapiJob(job_id)[0]
379 AssertEqual(len(result["handshake"]), 3)
380 AssertEqual(result["handshake"][0], constants.RIE_VERSION)
381 AssertEqual(len(result["x509_key_name"]), 3)
382 AssertIn("-----BEGIN CERTIFICATE-----", result["x509_ca"])
386 """Testing getting node(s) info via remote API.
389 def _VerifyNode(data):
390 for entry in NODE_FIELDS:
391 AssertIn(entry, data)
393 def _VerifyNodesList(data):
395 for entry in LIST_FIELDS:
396 AssertIn(entry, node)
398 def _VerifyNodesBulk(data):
399 for node_data in data:
400 _VerifyNode(node_data)
403 ("/2/nodes/%s" % node["primary"], _VerifyNode, "GET", None),
404 ("/2/nodes", _VerifyNodesList, "GET", None),
405 ("/2/nodes?bulk=1", _VerifyNodesBulk, "GET", None),
409 def TestTags(kind, name, tags):
410 """Tests .../tags resources.
413 if kind == constants.TAG_CLUSTER:
415 elif kind == constants.TAG_NODE:
416 uri = "/2/nodes/%s/tags" % name
417 elif kind == constants.TAG_INSTANCE:
418 uri = "/2/instances/%s/tags" % name
419 elif kind == constants.TAG_NODEGROUP:
420 uri = "/2/groups/%s/tags" % name
422 raise errors.ProgrammerError("Unknown tag kind")
424 def _VerifyTags(data):
425 AssertEqual(sorted(tags), sorted(data))
427 queryargs = "&".join("tag=%s" % i for i in tags)
430 (job_id, ) = _DoTests([
431 ("%s?%s" % (uri, queryargs), _VerifyReturnsJob, "PUT", None),
433 _WaitForRapiJob(job_id)
437 (uri, _VerifyTags, "GET", None),
441 (job_id, ) = _DoTests([
442 ("%s?%s" % (uri, queryargs), _VerifyReturnsJob, "DELETE", None),
444 _WaitForRapiJob(job_id)
447 def _WaitForRapiJob(job_id):
448 """Waits for a job to finish.
451 def _VerifyJob(data):
452 AssertEqual(data["id"], job_id)
453 for field in JOB_FIELDS:
454 AssertIn(field, data)
457 ("/2/jobs/%s" % job_id, _VerifyJob, "GET", None),
460 return rapi.client_utils.PollJob(_rapi_client, job_id,
461 cli.StdioJobPollReportCb())
464 def TestRapiNodeGroups():
465 """Test several node group operations using RAPI.
468 groups = qa_config.get("groups", {})
469 group1, group2, group3 = groups.get("inexistent-groups",
470 ["group1", "group2", "group3"])[:3]
472 # Create a group with no attributes
477 (job_id, ) = _DoTests([
478 ("/2/groups", _VerifyReturnsJob, "POST", body),
481 _WaitForRapiJob(job_id)
483 # Create a group specifying alloc_policy
486 "alloc_policy": constants.ALLOC_POLICY_UNALLOCABLE,
489 (job_id, ) = _DoTests([
490 ("/2/groups", _VerifyReturnsJob, "POST", body),
493 _WaitForRapiJob(job_id)
495 # Modify alloc_policy
497 "alloc_policy": constants.ALLOC_POLICY_UNALLOCABLE,
500 (job_id, ) = _DoTests([
501 ("/2/groups/%s/modify" % group1, _VerifyReturnsJob, "PUT", body),
504 _WaitForRapiJob(job_id)
511 (job_id, ) = _DoTests([
512 ("/2/groups/%s/rename" % group2, _VerifyReturnsJob, "PUT", body),
515 _WaitForRapiJob(job_id)
518 for group in [group1, group3]:
519 (job_id, ) = _DoTests([
520 ("/2/groups/%s" % group, _VerifyReturnsJob, "DELETE", None),
523 _WaitForRapiJob(job_id)
526 def TestRapiInstanceAdd(node, use_client):
527 """Test adding a new instance via RAPI"""
528 instance = qa_config.AcquireInstance()
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_MAXMEM: utils.ParseUnit(qa_config.get(constants.BE_MAXMEM)),
536 constants.BE_MINMEM: utils.ParseUnit(qa_config.get(constants.BE_MINMEM)),
540 job_id = _rapi_client.CreateInstance(constants.INSTANCE_CREATE,
544 os=qa_config.get("os"),
545 pnode=node["primary"],
550 "mode": constants.INSTANCE_CREATE,
551 "name": instance["name"],
552 "os_type": qa_config.get("os"),
553 "disk_template": constants.DT_PLAIN,
554 "pnode": node["primary"],
555 "beparams": beparams,
560 (job_id, ) = _DoTests([
561 ("/2/instances", _VerifyReturnsJob, "POST", body),
564 _WaitForRapiJob(job_id)
568 qa_config.ReleaseInstance(instance)
572 def TestRapiInstanceRemove(instance, use_client):
573 """Test removing instance via RAPI"""
575 job_id = _rapi_client.DeleteInstance(instance["name"])
577 (job_id, ) = _DoTests([
578 ("/2/instances/%s" % instance["name"], _VerifyReturnsJob, "DELETE", None),
581 _WaitForRapiJob(job_id)
583 qa_config.ReleaseInstance(instance)
586 def TestRapiInstanceMigrate(instance):
587 """Test migrating instance via RAPI"""
588 # Move to secondary node
589 _WaitForRapiJob(_rapi_client.MigrateInstance(instance["name"]))
590 # And back to previous primary
591 _WaitForRapiJob(_rapi_client.MigrateInstance(instance["name"]))
594 def TestRapiInstanceFailover(instance):
595 """Test failing over instance via RAPI"""
596 # Move to secondary node
597 _WaitForRapiJob(_rapi_client.FailoverInstance(instance["name"]))
598 # And back to previous primary
599 _WaitForRapiJob(_rapi_client.FailoverInstance(instance["name"]))
602 def TestRapiInstanceShutdown(instance):
603 """Test stopping an instance via RAPI"""
604 _WaitForRapiJob(_rapi_client.ShutdownInstance(instance["name"]))
607 def TestRapiInstanceStartup(instance):
608 """Test starting an instance via RAPI"""
609 _WaitForRapiJob(_rapi_client.StartupInstance(instance["name"]))
612 def TestRapiInstanceRenameAndBack(rename_source, rename_target):
613 """Test renaming instance via RAPI
615 This must leave the instance with the original name (in the
619 _WaitForRapiJob(_rapi_client.RenameInstance(rename_source, rename_target))
620 _WaitForRapiJob(_rapi_client.RenameInstance(rename_target, rename_source))
623 def TestRapiInstanceReinstall(instance):
624 """Test reinstalling an instance via RAPI"""
625 _WaitForRapiJob(_rapi_client.ReinstallInstance(instance["name"]))
628 def TestRapiInstanceReplaceDisks(instance):
629 """Test replacing instance disks via RAPI"""
630 _WaitForRapiJob(_rapi_client.ReplaceInstanceDisks(instance["name"],
631 mode=constants.REPLACE_DISK_AUTO, disks=[]))
632 _WaitForRapiJob(_rapi_client.ReplaceInstanceDisks(instance["name"],
633 mode=constants.REPLACE_DISK_SEC, disks="0"))
636 def TestRapiInstanceModify(instance):
637 """Test modifying instance via RAPI"""
638 def _ModifyInstance(**kwargs):
639 _WaitForRapiJob(_rapi_client.ModifyInstance(instance["name"], **kwargs))
641 _ModifyInstance(hvparams={
642 constants.HV_KERNEL_ARGS: "single",
645 _ModifyInstance(beparams={
646 constants.BE_VCPUS: 3,
649 _ModifyInstance(beparams={
650 constants.BE_VCPUS: constants.VALUE_DEFAULT,
653 _ModifyInstance(hvparams={
654 constants.HV_KERNEL_ARGS: constants.VALUE_DEFAULT,
658 def TestRapiInstanceConsole(instance):
659 """Test getting instance console information via RAPI"""
660 result = _rapi_client.GetInstanceConsole(instance["name"])
661 console = objects.InstanceConsole.FromDict(result)
662 AssertEqual(console.Validate(), True)
663 AssertEqual(console.instance, qa_utils.ResolveInstanceName(instance["name"]))
666 def TestRapiStoppedInstanceConsole(instance):
667 """Test getting stopped instance's console information via RAPI"""
669 _rapi_client.GetInstanceConsole(instance["name"])
670 except rapi.client.GanetiApiError, err:
671 AssertEqual(err.code, 503)
673 raise qa_error.Error("Getting console for stopped instance didn't"
677 def GetOperatingSystems():
678 """Retrieves a list of all available operating systems.
681 return _rapi_client.GetOperatingSystems()
684 def TestInterClusterInstanceMove(src_instance, dest_instance,
685 pnode, snode, tnode):
686 """Test tools/move-instance"""
687 master = qa_config.GetMasterNode()
689 rapi_pw_file = tempfile.NamedTemporaryFile()
690 rapi_pw_file.write(_rapi_password)
693 # TODO: Run some instance tests before moving back
696 # instance is not redundant, but we still need to pass a node
697 # (which will be ignored)
701 # note: pnode:snode are the *current* nodes, so we move it first to
702 # tnode:pnode, then back to pnode:snode
703 for si, di, pn, sn in [(src_instance["name"], dest_instance["name"],
704 tnode["primary"], pnode["primary"]),
705 (dest_instance["name"], src_instance["name"],
706 pnode["primary"], fsec["primary"])]:
708 "../tools/move-instance",
710 "--src-ca-file=%s" % _rapi_ca.name,
711 "--src-username=%s" % _rapi_username,
712 "--src-password-file=%s" % rapi_pw_file.name,
713 "--dest-instance-name=%s" % di,
714 "--dest-primary-node=%s" % pn,
715 "--dest-secondary-node=%s" % sn,
716 "--net=0:mac=%s" % constants.VALUE_GENERATE,
722 AssertEqual(StartLocalCommand(cmd).wait(), 0)