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)
47 from qa_utils import InstanceCheck, INST_DOWN, INST_UP, FIRST_ARG
56 def Setup(username, password):
57 """Configures the RAPI client.
60 # pylint: disable=W0603
67 _rapi_username = username
68 _rapi_password = password
70 master = qa_config.GetMasterNode()
72 # Load RAPI certificate from master node
73 cmd = ["cat", constants.RAPI_CERT_FILE]
75 # Write to temporary file
76 _rapi_ca = tempfile.NamedTemporaryFile()
77 _rapi_ca.write(qa_utils.GetCommandOutput(master["primary"],
78 utils.ShellQuoteArgs(cmd)))
81 port = qa_config.get("rapi-port", default=constants.DEFAULT_RAPI_PORT)
82 cfg_curl = rapi.client.GenericCurlConfig(cafile=_rapi_ca.name,
85 _rapi_client = rapi.client.GanetiRapiClient(master["primary"], port=port,
88 curl_config_fn=cfg_curl)
90 print "RAPI protocol version: %s" % _rapi_client.GetVersion()
93 INSTANCE_FIELDS = ("name", "os", "pnode", "snodes",
95 "disk_template", "disk.sizes",
96 "nic.ips", "nic.macs", "nic.modes", "nic.links",
97 "beparams", "hvparams",
98 "oper_state", "oper_ram", "oper_vcpus", "status", "tags")
100 NODE_FIELDS = ("name", "dtotal", "dfree",
101 "mtotal", "mnode", "mfree",
102 "pinst_cnt", "sinst_cnt", "tags")
104 GROUP_FIELDS = frozenset([
107 "node_cnt", "node_list",
110 JOB_FIELDS = frozenset([
111 "id", "ops", "status", "summary",
112 "opstatus", "opresult", "oplog",
113 "received_ts", "start_ts", "end_ts",
116 LIST_FIELDS = ("id", "uri")
120 """Return whether remote API tests should be run.
123 return qa_config.TestEnabled("rapi")
127 # pylint: disable=W0212
128 # due to _SendRequest usage
131 for uri, verify, method, body in uris:
132 assert uri.startswith("/")
134 print "%s %s" % (method, uri)
135 data = _rapi_client._SendRequest(method, uri, None, body)
137 if verify is not None:
141 AssertEqual(data, verify)
148 def _VerifyReturnsJob(data):
149 AssertMatch(data, r"^\d+$")
153 """Testing remote API version.
157 ("/version", constants.RAPI_VERSION, "GET", None),
161 def TestEmptyCluster():
162 """Testing remote API on an empty cluster.
165 master = qa_config.GetMasterNode()
166 master_full = qa_utils.ResolveNodeName(master)
168 def _VerifyInfo(data):
169 AssertIn("name", data)
170 AssertIn("master", data)
171 AssertEqual(data["master"], master_full)
173 def _VerifyNodes(data):
176 "uri": "/2/nodes/%s" % master_full,
178 AssertIn(master_entry, data)
180 def _VerifyNodesBulk(data):
182 for entry in NODE_FIELDS:
183 AssertIn(entry, node)
185 def _VerifyGroups(data):
187 "name": constants.INITIAL_NODE_GROUP_NAME,
188 "uri": "/2/groups/" + constants.INITIAL_NODE_GROUP_NAME,
190 AssertIn(default_group, data)
192 def _VerifyGroupsBulk(data):
194 for field in GROUP_FIELDS:
195 AssertIn(field, group)
198 ("/", None, "GET", None),
199 ("/2/info", _VerifyInfo, "GET", None),
200 ("/2/tags", None, "GET", None),
201 ("/2/nodes", _VerifyNodes, "GET", None),
202 ("/2/nodes?bulk=1", _VerifyNodesBulk, "GET", None),
203 ("/2/groups", _VerifyGroups, "GET", None),
204 ("/2/groups?bulk=1", _VerifyGroupsBulk, "GET", None),
205 ("/2/instances", [], "GET", None),
206 ("/2/instances?bulk=1", [], "GET", None),
207 ("/2/os", None, "GET", None),
210 # Test HTTP Not Found
211 for method in ["GET", "PUT", "POST", "DELETE"]:
213 _DoTests([("/99/resource/not/here/99", None, method, None)])
214 except rapi.client.GanetiApiError, err:
215 AssertEqual(err.code, 404)
217 raise qa_error.Error("Non-existent resource didn't return HTTP 404")
219 # Test HTTP Not Implemented
220 for method in ["PUT", "POST", "DELETE"]:
222 _DoTests([("/version", None, method, None)])
223 except rapi.client.GanetiApiError, err:
224 AssertEqual(err.code, 501)
226 raise qa_error.Error("Non-implemented method didn't fail")
230 """Testing resource queries via remote API.
233 master_name = qa_utils.ResolveNodeName(qa_config.GetMasterNode())
234 rnd = random.Random(7818)
236 for what in constants.QR_VIA_RAPI:
237 if what == constants.QR_JOB:
239 elif what == constants.QR_EXPORT:
244 all_fields = query.ALL_FIELDS[what].keys()
245 rnd.shuffle(all_fields)
247 # No fields, should return everything
248 result = _rapi_client.QueryFields(what)
249 qresult = objects.QueryFieldsResponse.FromDict(result)
250 AssertEqual(len(qresult.fields), len(all_fields))
253 result = _rapi_client.QueryFields(what, fields=[namefield])
254 qresult = objects.QueryFieldsResponse.FromDict(result)
255 AssertEqual(len(qresult.fields), 1)
257 # Specify all fields, order must be correct
258 result = _rapi_client.QueryFields(what, fields=all_fields)
259 qresult = objects.QueryFieldsResponse.FromDict(result)
260 AssertEqual(len(qresult.fields), len(all_fields))
261 AssertEqual([fdef.name for fdef in qresult.fields], all_fields)
264 result = _rapi_client.QueryFields(what, fields=["_unknown!"])
265 qresult = objects.QueryFieldsResponse.FromDict(result)
266 AssertEqual(len(qresult.fields), 1)
267 AssertEqual(qresult.fields[0].name, "_unknown!")
268 AssertEqual(qresult.fields[0].kind, constants.QFT_UNKNOWN)
270 # Try once more, this time without the client
272 ("/2/query/%s/fields" % what, None, "GET", None),
273 ("/2/query/%s/fields?fields=name,name,%s" % (what, all_fields[0]),
277 # Try missing query argument
280 ("/2/query/%s" % what, None, "GET", None),
282 except rapi.client.GanetiApiError, err:
283 AssertEqual(err.code, 400)
285 raise qa_error.Error("Request missing 'fields' parameter didn't fail")
287 def _Check(exp_fields, data):
288 qresult = objects.QueryResponse.FromDict(data)
289 AssertEqual([fdef.name for fdef in qresult.fields], exp_fields)
290 if not isinstance(qresult.data, list):
291 raise qa_error.Error("Query did not return a list")
294 # Specify fields in query
295 ("/2/query/%s?fields=%s" % (what, ",".join(all_fields)),
296 compat.partial(_Check, all_fields), "GET", None),
298 ("/2/query/%s?fields=%s" % (what, namefield),
299 compat.partial(_Check, [namefield]), "GET", None),
302 ("/2/query/%s?fields=%s,%%20%s%%09,%s%%20" %
303 (what, namefield, namefield, namefield),
304 compat.partial(_Check, [namefield] * 3), "GET", None),
306 # PUT with fields in query
307 ("/2/query/%s?fields=%s" % (what, namefield),
308 compat.partial(_Check, [namefield]), "PUT", {}),
311 ("/2/query/%s" % what, compat.partial(_Check, all_fields), "PUT", {
312 "fields": all_fields,
315 ("/2/query/%s" % what, compat.partial(_Check, [namefield] * 4), "PUT", {
316 "fields": [namefield] * 4,
323 ("/2/query/%s" % what, compat.partial(_Check, all_fields), "PUT", {
324 "fields": all_fields,
325 "filter": [qlang.OP_TRUE, namefield],
329 if what == constants.QR_LOCK:
330 # Locks can't be filtered
333 except rapi.client.GanetiApiError, err:
334 AssertEqual(err.code, 500)
336 raise qa_error.Error("Filtering locks didn't fail")
340 if what == constants.QR_NODE:
342 (nodes, ) = _DoTests([("/2/query/%s" % what,
343 compat.partial(_Check, ["name", "master"]), "PUT", {
344 "fields": ["name", "master"],
345 "filter": [qlang.OP_TRUE, "master"],
347 qresult = objects.QueryResponse.FromDict(nodes)
348 AssertEqual(qresult.data, [
349 [[constants.RS_NORMAL, master_name], [constants.RS_NORMAL, True]],
353 @InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
354 def TestInstance(instance):
355 """Testing getting instance(s) info via remote API.
358 def _VerifyInstance(data):
359 for entry in INSTANCE_FIELDS:
360 AssertIn(entry, data)
362 def _VerifyInstancesList(data):
363 for instance in data:
364 for entry in LIST_FIELDS:
365 AssertIn(entry, instance)
367 def _VerifyInstancesBulk(data):
368 for instance_data in data:
369 _VerifyInstance(instance_data)
372 ("/2/instances/%s" % instance["name"], _VerifyInstance, "GET", None),
373 ("/2/instances", _VerifyInstancesList, "GET", None),
374 ("/2/instances?bulk=1", _VerifyInstancesBulk, "GET", None),
375 ("/2/instances/%s/activate-disks" % instance["name"],
376 _VerifyReturnsJob, "PUT", None),
377 ("/2/instances/%s/deactivate-disks" % instance["name"],
378 _VerifyReturnsJob, "PUT", None),
381 # Test OpBackupPrepare
382 (job_id, ) = _DoTests([
383 ("/2/instances/%s/prepare-export?mode=%s" %
384 (instance["name"], constants.EXPORT_MODE_REMOTE),
385 _VerifyReturnsJob, "PUT", None),
388 result = _WaitForRapiJob(job_id)[0]
389 AssertEqual(len(result["handshake"]), 3)
390 AssertEqual(result["handshake"][0], constants.RIE_VERSION)
391 AssertEqual(len(result["x509_key_name"]), 3)
392 AssertIn("-----BEGIN CERTIFICATE-----", result["x509_ca"])
396 """Testing getting node(s) info via remote API.
399 def _VerifyNode(data):
400 for entry in NODE_FIELDS:
401 AssertIn(entry, data)
403 def _VerifyNodesList(data):
405 for entry in LIST_FIELDS:
406 AssertIn(entry, node)
408 def _VerifyNodesBulk(data):
409 for node_data in data:
410 _VerifyNode(node_data)
413 ("/2/nodes/%s" % node["primary"], _VerifyNode, "GET", None),
414 ("/2/nodes", _VerifyNodesList, "GET", None),
415 ("/2/nodes?bulk=1", _VerifyNodesBulk, "GET", None),
419 def TestTags(kind, name, tags):
420 """Tests .../tags resources.
423 if kind == constants.TAG_CLUSTER:
425 elif kind == constants.TAG_NODE:
426 uri = "/2/nodes/%s/tags" % name
427 elif kind == constants.TAG_INSTANCE:
428 uri = "/2/instances/%s/tags" % name
429 elif kind == constants.TAG_NODEGROUP:
430 uri = "/2/groups/%s/tags" % name
432 raise errors.ProgrammerError("Unknown tag kind")
434 def _VerifyTags(data):
435 AssertEqual(sorted(tags), sorted(data))
437 queryargs = "&".join("tag=%s" % i for i in tags)
440 (job_id, ) = _DoTests([
441 ("%s?%s" % (uri, queryargs), _VerifyReturnsJob, "PUT", None),
443 _WaitForRapiJob(job_id)
447 (uri, _VerifyTags, "GET", None),
451 (job_id, ) = _DoTests([
452 ("%s?%s" % (uri, queryargs), _VerifyReturnsJob, "DELETE", None),
454 _WaitForRapiJob(job_id)
457 def _WaitForRapiJob(job_id):
458 """Waits for a job to finish.
461 def _VerifyJob(data):
462 AssertEqual(data["id"], job_id)
463 for field in JOB_FIELDS:
464 AssertIn(field, data)
467 ("/2/jobs/%s" % job_id, _VerifyJob, "GET", None),
470 return rapi.client_utils.PollJob(_rapi_client, job_id,
471 cli.StdioJobPollReportCb())
474 def TestRapiNodeGroups():
475 """Test several node group operations using RAPI.
478 groups = qa_config.get("groups", {})
479 group1, group2, group3 = groups.get("inexistent-groups",
480 ["group1", "group2", "group3"])[:3]
482 # Create a group with no attributes
487 (job_id, ) = _DoTests([
488 ("/2/groups", _VerifyReturnsJob, "POST", body),
491 _WaitForRapiJob(job_id)
493 # Create a group specifying alloc_policy
496 "alloc_policy": constants.ALLOC_POLICY_UNALLOCABLE,
499 (job_id, ) = _DoTests([
500 ("/2/groups", _VerifyReturnsJob, "POST", body),
503 _WaitForRapiJob(job_id)
505 # Modify alloc_policy
507 "alloc_policy": constants.ALLOC_POLICY_UNALLOCABLE,
510 (job_id, ) = _DoTests([
511 ("/2/groups/%s/modify" % group1, _VerifyReturnsJob, "PUT", body),
514 _WaitForRapiJob(job_id)
521 (job_id, ) = _DoTests([
522 ("/2/groups/%s/rename" % group2, _VerifyReturnsJob, "PUT", body),
525 _WaitForRapiJob(job_id)
528 for group in [group1, group3]:
529 (job_id, ) = _DoTests([
530 ("/2/groups/%s" % group, _VerifyReturnsJob, "DELETE", None),
533 _WaitForRapiJob(job_id)
536 def TestRapiInstanceAdd(node, use_client):
537 """Test adding a new instance via RAPI"""
538 instance = qa_config.AcquireInstance()
540 disk_sizes = [utils.ParseUnit(size) for size in qa_config.get("disk")]
541 disks = [{"size": size} for size in disk_sizes]
545 constants.BE_MAXMEM: utils.ParseUnit(qa_config.get(constants.BE_MAXMEM)),
546 constants.BE_MINMEM: utils.ParseUnit(qa_config.get(constants.BE_MINMEM)),
550 job_id = _rapi_client.CreateInstance(constants.INSTANCE_CREATE,
554 os=qa_config.get("os"),
555 pnode=node["primary"],
560 "mode": constants.INSTANCE_CREATE,
561 "name": instance["name"],
562 "os_type": qa_config.get("os"),
563 "disk_template": constants.DT_PLAIN,
564 "pnode": node["primary"],
565 "beparams": beparams,
570 (job_id, ) = _DoTests([
571 ("/2/instances", _VerifyReturnsJob, "POST", body),
574 _WaitForRapiJob(job_id)
578 qa_config.ReleaseInstance(instance)
582 @InstanceCheck(None, INST_DOWN, FIRST_ARG)
583 def TestRapiInstanceRemove(instance, use_client):
584 """Test removing instance via RAPI"""
586 job_id = _rapi_client.DeleteInstance(instance["name"])
588 (job_id, ) = _DoTests([
589 ("/2/instances/%s" % instance["name"], _VerifyReturnsJob, "DELETE", None),
592 _WaitForRapiJob(job_id)
594 qa_config.ReleaseInstance(instance)
597 @InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
598 def TestRapiInstanceMigrate(instance):
599 """Test migrating instance via RAPI"""
600 # Move to secondary node
601 _WaitForRapiJob(_rapi_client.MigrateInstance(instance["name"]))
602 qa_utils.RunInstanceCheck(instance, True)
603 # And back to previous primary
604 _WaitForRapiJob(_rapi_client.MigrateInstance(instance["name"]))
607 @InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
608 def TestRapiInstanceFailover(instance):
609 """Test failing over instance via RAPI"""
610 # Move to secondary node
611 _WaitForRapiJob(_rapi_client.FailoverInstance(instance["name"]))
612 qa_utils.RunInstanceCheck(instance, True)
613 # And back to previous primary
614 _WaitForRapiJob(_rapi_client.FailoverInstance(instance["name"]))
617 @InstanceCheck(INST_UP, INST_DOWN, FIRST_ARG)
618 def TestRapiInstanceShutdown(instance):
619 """Test stopping an instance via RAPI"""
620 _WaitForRapiJob(_rapi_client.ShutdownInstance(instance["name"]))
623 @InstanceCheck(INST_DOWN, INST_UP, FIRST_ARG)
624 def TestRapiInstanceStartup(instance):
625 """Test starting an instance via RAPI"""
626 _WaitForRapiJob(_rapi_client.StartupInstance(instance["name"]))
629 @InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
630 def TestRapiInstanceRenameAndBack(rename_source, rename_target):
631 """Test renaming instance via RAPI
633 This must leave the instance with the original name (in the
637 _WaitForRapiJob(_rapi_client.RenameInstance(rename_source, rename_target))
638 qa_utils.RunInstanceCheck(rename_source, False)
639 qa_utils.RunInstanceCheck(rename_target, True)
640 _WaitForRapiJob(_rapi_client.RenameInstance(rename_target, rename_source))
641 qa_utils.RunInstanceCheck(rename_target, False)
644 @InstanceCheck(INST_DOWN, INST_DOWN, FIRST_ARG)
645 def TestRapiInstanceReinstall(instance):
646 """Test reinstalling an instance via RAPI"""
647 _WaitForRapiJob(_rapi_client.ReinstallInstance(instance["name"]))
650 @InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
651 def TestRapiInstanceReplaceDisks(instance):
652 """Test replacing instance disks via RAPI"""
653 _WaitForRapiJob(_rapi_client.ReplaceInstanceDisks(instance["name"],
654 mode=constants.REPLACE_DISK_AUTO, disks=[]))
655 _WaitForRapiJob(_rapi_client.ReplaceInstanceDisks(instance["name"],
656 mode=constants.REPLACE_DISK_SEC, disks="0"))
659 @InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
660 def TestRapiInstanceModify(instance):
661 """Test modifying instance via RAPI"""
662 def _ModifyInstance(**kwargs):
663 _WaitForRapiJob(_rapi_client.ModifyInstance(instance["name"], **kwargs))
665 _ModifyInstance(hvparams={
666 constants.HV_KERNEL_ARGS: "single",
669 _ModifyInstance(beparams={
670 constants.BE_VCPUS: 3,
673 _ModifyInstance(beparams={
674 constants.BE_VCPUS: constants.VALUE_DEFAULT,
677 _ModifyInstance(hvparams={
678 constants.HV_KERNEL_ARGS: constants.VALUE_DEFAULT,
682 @InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
683 def TestRapiInstanceConsole(instance):
684 """Test getting instance console information via RAPI"""
685 result = _rapi_client.GetInstanceConsole(instance["name"])
686 console = objects.InstanceConsole.FromDict(result)
687 AssertEqual(console.Validate(), True)
688 AssertEqual(console.instance, qa_utils.ResolveInstanceName(instance["name"]))
691 @InstanceCheck(INST_DOWN, INST_DOWN, FIRST_ARG)
692 def TestRapiStoppedInstanceConsole(instance):
693 """Test getting stopped instance's console information via RAPI"""
695 _rapi_client.GetInstanceConsole(instance["name"])
696 except rapi.client.GanetiApiError, err:
697 AssertEqual(err.code, 503)
699 raise qa_error.Error("Getting console for stopped instance didn't"
703 def GetOperatingSystems():
704 """Retrieves a list of all available operating systems.
707 return _rapi_client.GetOperatingSystems()
710 def TestInterClusterInstanceMove(src_instance, dest_instance,
711 pnode, snode, tnode):
712 """Test tools/move-instance"""
713 master = qa_config.GetMasterNode()
715 rapi_pw_file = tempfile.NamedTemporaryFile()
716 rapi_pw_file.write(_rapi_password)
719 # TODO: Run some instance tests before moving back
722 # instance is not redundant, but we still need to pass a node
723 # (which will be ignored)
727 # note: pnode:snode are the *current* nodes, so we move it first to
728 # tnode:pnode, then back to pnode:snode
729 for si, di, pn, sn in [(src_instance["name"], dest_instance["name"],
730 tnode["primary"], pnode["primary"]),
731 (dest_instance["name"], src_instance["name"],
732 pnode["primary"], fsec["primary"])]:
734 "../tools/move-instance",
736 "--src-ca-file=%s" % _rapi_ca.name,
737 "--src-username=%s" % _rapi_username,
738 "--src-password-file=%s" % rapi_pw_file.name,
739 "--dest-instance-name=%s" % di,
740 "--dest-primary-node=%s" % pn,
741 "--dest-secondary-node=%s" % sn,
742 "--net=0:mac=%s" % constants.VALUE_GENERATE,
748 qa_utils.RunInstanceCheck(di, False)
749 AssertEqual(StartLocalCommand(cmd).wait(), 0)
750 qa_utils.RunInstanceCheck(si, False)
751 qa_utils.RunInstanceCheck(di, True)