QA: Ensure the DRBD secret is not retrievable via RAPI
[ganeti-github.git] / qa / qa_rapi.py
1 #
2 #
3
4 # Copyright (C) 2007, 2008, 2009, 2010, 2011, 2012, 2013 Google Inc.
5 #
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.
10 #
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.
15 #
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
19 # 02110-1301, USA.
20
21
22 """Remote API QA tests.
23
24 """
25
26 import tempfile
27 import random
28 import re
29 import itertools
30 import functools
31
32 from ganeti import utils
33 from ganeti import constants
34 from ganeti import errors
35 from ganeti import cli
36 from ganeti import rapi
37 from ganeti import objects
38 from ganeti import query
39 from ganeti import compat
40 from ganeti import qlang
41 from ganeti import pathutils
42
43 import ganeti.rapi.client # pylint: disable=W0611
44 import ganeti.rapi.client_utils
45
46 import qa_config
47 import qa_error
48 import qa_logging
49 import qa_utils
50
51 from qa_instance import GetInstanceInfo
52 from qa_instance import IsFailoverSupported
53 from qa_instance import IsMigrationSupported
54 from qa_instance import IsDiskReplacingSupported
55 from qa_utils import (AssertEqual, AssertIn, AssertMatch, StartLocalCommand)
56 from qa_utils import InstanceCheck, INST_DOWN, INST_UP, FIRST_ARG
57
58
59 _rapi_ca = None
60 _rapi_client = None
61 _rapi_username = None
62 _rapi_password = None
63
64
65 def Setup(username, password):
66 """Configures the RAPI client.
67
68 """
69 # pylint: disable=W0603
70 # due to global usage
71 global _rapi_ca
72 global _rapi_client
73 global _rapi_username
74 global _rapi_password
75
76 _rapi_username = username
77 _rapi_password = password
78
79 master = qa_config.GetMasterNode()
80
81 # Load RAPI certificate from master node
82 cmd = ["cat", qa_utils.MakeNodePath(master, pathutils.RAPI_CERT_FILE)]
83
84 # Write to temporary file
85 _rapi_ca = tempfile.NamedTemporaryFile()
86 _rapi_ca.write(qa_utils.GetCommandOutput(master.primary,
87 utils.ShellQuoteArgs(cmd)))
88 _rapi_ca.flush()
89
90 port = qa_config.get("rapi-port", default=constants.DEFAULT_RAPI_PORT)
91 cfg_curl = rapi.client.GenericCurlConfig(cafile=_rapi_ca.name,
92 proxy="")
93
94 if qa_config.UseVirtualCluster():
95 # TODO: Implement full support for RAPI on virtual clusters
96 print qa_logging.FormatWarning("RAPI tests are not yet supported on"
97 " virtual clusters and will be disabled")
98
99 assert _rapi_client is None
100 else:
101 _rapi_client = rapi.client.GanetiRapiClient(master.primary, port=port,
102 username=username,
103 password=password,
104 curl_config_fn=cfg_curl)
105
106 print "RAPI protocol version: %s" % _rapi_client.GetVersion()
107
108
109 INSTANCE_FIELDS = ("name", "os", "pnode", "snodes",
110 "admin_state",
111 "disk_template", "disk.sizes", "disk.spindles",
112 "nic.ips", "nic.macs", "nic.modes", "nic.links",
113 "beparams", "hvparams",
114 "oper_state", "oper_ram", "oper_vcpus", "status", "tags")
115
116 NODE_FIELDS = ("name", "dtotal", "dfree", "sptotal", "spfree",
117 "mtotal", "mnode", "mfree",
118 "pinst_cnt", "sinst_cnt", "tags")
119
120 GROUP_FIELDS = compat.UniqueFrozenset([
121 "name", "uuid",
122 "alloc_policy",
123 "node_cnt", "node_list",
124 ])
125
126 JOB_FIELDS = compat.UniqueFrozenset([
127 "id", "ops", "status", "summary",
128 "opstatus", "opresult", "oplog",
129 "received_ts", "start_ts", "end_ts",
130 ])
131
132 LIST_FIELDS = ("id", "uri")
133
134
135 def Enabled():
136 """Return whether remote API tests should be run.
137
138 """
139 # TODO: Implement RAPI tests for virtual clusters
140 return (qa_config.TestEnabled("rapi") and
141 not qa_config.UseVirtualCluster())
142
143
144 def _DoTests(uris):
145 # pylint: disable=W0212
146 # due to _SendRequest usage
147 results = []
148
149 for uri, verify, method, body in uris:
150 assert uri.startswith("/")
151
152 print "%s %s" % (method, uri)
153 data = _rapi_client._SendRequest(method, uri, None, body)
154
155 if verify is not None:
156 if callable(verify):
157 verify(data)
158 else:
159 AssertEqual(data, verify)
160
161 results.append(data)
162
163 return results
164
165
166 def _VerifyReturnsJob(data):
167 if not isinstance(data, int):
168 AssertMatch(data, r"^\d+$")
169
170
171 def TestVersion():
172 """Testing remote API version.
173
174 """
175 _DoTests([
176 ("/version", constants.RAPI_VERSION, "GET", None),
177 ])
178
179
180 def TestEmptyCluster():
181 """Testing remote API on an empty cluster.
182
183 """
184 master = qa_config.GetMasterNode()
185 master_full = qa_utils.ResolveNodeName(master)
186
187 def _VerifyInfo(data):
188 AssertIn("name", data)
189 AssertIn("master", data)
190 AssertEqual(data["master"], master_full)
191
192 def _VerifyNodes(data):
193 master_entry = {
194 "id": master_full,
195 "uri": "/2/nodes/%s" % master_full,
196 }
197 AssertIn(master_entry, data)
198
199 def _VerifyNodesBulk(data):
200 for node in data:
201 for entry in NODE_FIELDS:
202 AssertIn(entry, node)
203
204 def _VerifyGroups(data):
205 default_group = {
206 "name": constants.INITIAL_NODE_GROUP_NAME,
207 "uri": "/2/groups/" + constants.INITIAL_NODE_GROUP_NAME,
208 }
209 AssertIn(default_group, data)
210
211 def _VerifyGroupsBulk(data):
212 for group in data:
213 for field in GROUP_FIELDS:
214 AssertIn(field, group)
215
216 _DoTests([
217 ("/", None, "GET", None),
218 ("/2/info", _VerifyInfo, "GET", None),
219 ("/2/tags", None, "GET", None),
220 ("/2/nodes", _VerifyNodes, "GET", None),
221 ("/2/nodes?bulk=1", _VerifyNodesBulk, "GET", None),
222 ("/2/groups", _VerifyGroups, "GET", None),
223 ("/2/groups?bulk=1", _VerifyGroupsBulk, "GET", None),
224 ("/2/instances", [], "GET", None),
225 ("/2/instances?bulk=1", [], "GET", None),
226 ("/2/os", None, "GET", None),
227 ])
228
229 # Test HTTP Not Found
230 for method in ["GET", "PUT", "POST", "DELETE"]:
231 try:
232 _DoTests([("/99/resource/not/here/99", None, method, None)])
233 except rapi.client.GanetiApiError, err:
234 AssertEqual(err.code, 404)
235 else:
236 raise qa_error.Error("Non-existent resource didn't return HTTP 404")
237
238 # Test HTTP Not Implemented
239 for method in ["PUT", "POST", "DELETE"]:
240 try:
241 _DoTests([("/version", None, method, None)])
242 except rapi.client.GanetiApiError, err:
243 AssertEqual(err.code, 501)
244 else:
245 raise qa_error.Error("Non-implemented method didn't fail")
246
247
248 def TestRapiQuery():
249 """Testing resource queries via remote API.
250
251 """
252 # FIXME: the tests are failing if no LVM is enabled, investigate
253 # if it is a bug in the QA or in the code
254 if not qa_config.IsStorageTypeSupported(constants.ST_LVM_VG):
255 return
256
257 master_name = qa_utils.ResolveNodeName(qa_config.GetMasterNode())
258 rnd = random.Random(7818)
259
260 for what in constants.QR_VIA_RAPI:
261 if what == constants.QR_JOB:
262 namefield = "id"
263 elif what == constants.QR_EXPORT:
264 namefield = "export"
265 else:
266 namefield = "name"
267
268 all_fields = query.ALL_FIELDS[what].keys()
269 rnd.shuffle(all_fields)
270
271 # No fields, should return everything
272 result = _rapi_client.QueryFields(what)
273 qresult = objects.QueryFieldsResponse.FromDict(result)
274 AssertEqual(len(qresult.fields), len(all_fields))
275
276 # One field
277 result = _rapi_client.QueryFields(what, fields=[namefield])
278 qresult = objects.QueryFieldsResponse.FromDict(result)
279 AssertEqual(len(qresult.fields), 1)
280
281 # Specify all fields, order must be correct
282 result = _rapi_client.QueryFields(what, fields=all_fields)
283 qresult = objects.QueryFieldsResponse.FromDict(result)
284 AssertEqual(len(qresult.fields), len(all_fields))
285 AssertEqual([fdef.name for fdef in qresult.fields], all_fields)
286
287 # Unknown field
288 result = _rapi_client.QueryFields(what, fields=["_unknown!"])
289 qresult = objects.QueryFieldsResponse.FromDict(result)
290 AssertEqual(len(qresult.fields), 1)
291 AssertEqual(qresult.fields[0].name, "_unknown!")
292 AssertEqual(qresult.fields[0].kind, constants.QFT_UNKNOWN)
293
294 # Try once more, this time without the client
295 _DoTests([
296 ("/2/query/%s/fields" % what, None, "GET", None),
297 ("/2/query/%s/fields?fields=name,name,%s" % (what, all_fields[0]),
298 None, "GET", None),
299 ])
300
301 # Try missing query argument
302 try:
303 _DoTests([
304 ("/2/query/%s" % what, None, "GET", None),
305 ])
306 except rapi.client.GanetiApiError, err:
307 AssertEqual(err.code, 400)
308 else:
309 raise qa_error.Error("Request missing 'fields' parameter didn't fail")
310
311 def _Check(exp_fields, data):
312 qresult = objects.QueryResponse.FromDict(data)
313 AssertEqual([fdef.name for fdef in qresult.fields], exp_fields)
314 if not isinstance(qresult.data, list):
315 raise qa_error.Error("Query did not return a list")
316
317 _DoTests([
318 # Specify fields in query
319 ("/2/query/%s?fields=%s" % (what, ",".join(all_fields)),
320 compat.partial(_Check, all_fields), "GET", None),
321
322 ("/2/query/%s?fields=%s" % (what, namefield),
323 compat.partial(_Check, [namefield]), "GET", None),
324
325 # Note the spaces
326 ("/2/query/%s?fields=%s,%%20%s%%09,%s%%20" %
327 (what, namefield, namefield, namefield),
328 compat.partial(_Check, [namefield] * 3), "GET", None),
329
330 # PUT with fields in query
331 ("/2/query/%s?fields=%s" % (what, namefield),
332 compat.partial(_Check, [namefield]), "PUT", {}),
333
334 ("/2/query/%s" % what, compat.partial(_Check, [namefield] * 4), "PUT", {
335 "fields": [namefield] * 4,
336 }),
337
338 ("/2/query/%s" % what, compat.partial(_Check, all_fields), "PUT", {
339 "fields": all_fields,
340 }),
341
342 ("/2/query/%s" % what, compat.partial(_Check, [namefield] * 4), "PUT", {
343 "fields": [namefield] * 4
344 })])
345
346 def _CheckFilter():
347 _DoTests([
348 # With filter
349 ("/2/query/%s" % what, compat.partial(_Check, all_fields), "PUT", {
350 "fields": all_fields,
351 "filter": [qlang.OP_TRUE, namefield],
352 }),
353 ])
354
355 if what == constants.QR_LOCK:
356 # Locks can't be filtered
357 try:
358 _CheckFilter()
359 except rapi.client.GanetiApiError, err:
360 AssertEqual(err.code, 500)
361 else:
362 raise qa_error.Error("Filtering locks didn't fail")
363 else:
364 _CheckFilter()
365
366 if what == constants.QR_NODE:
367 # Test with filter
368 (nodes, ) = _DoTests(
369 [("/2/query/%s" % what,
370 compat.partial(_Check, ["name", "master"]), "PUT",
371 {"fields": ["name", "master"],
372 "filter": [qlang.OP_TRUE, "master"],
373 })])
374 qresult = objects.QueryResponse.FromDict(nodes)
375 AssertEqual(qresult.data, [
376 [[constants.RS_NORMAL, master_name], [constants.RS_NORMAL, True]],
377 ])
378
379
380 @InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
381 def TestInstance(instance):
382 """Testing getting instance(s) info via remote API.
383
384 """
385 def _VerifyInstance(data):
386 for entry in INSTANCE_FIELDS:
387 AssertIn(entry, data)
388
389 def _VerifyInstancesList(data):
390 for instance in data:
391 for entry in LIST_FIELDS:
392 AssertIn(entry, instance)
393
394 def _VerifyInstancesBulk(data):
395 for instance_data in data:
396 _VerifyInstance(instance_data)
397
398 _DoTests([
399 ("/2/instances/%s" % instance.name, _VerifyInstance, "GET", None),
400 ("/2/instances", _VerifyInstancesList, "GET", None),
401 ("/2/instances?bulk=1", _VerifyInstancesBulk, "GET", None),
402 ("/2/instances/%s/activate-disks" % instance.name,
403 _VerifyReturnsJob, "PUT", None),
404 ("/2/instances/%s/deactivate-disks" % instance.name,
405 _VerifyReturnsJob, "PUT", None),
406 ])
407
408 # Test OpBackupPrepare
409 (job_id, ) = _DoTests([
410 ("/2/instances/%s/prepare-export?mode=%s" %
411 (instance.name, constants.EXPORT_MODE_REMOTE),
412 _VerifyReturnsJob, "PUT", None),
413 ])
414
415 result = _WaitForRapiJob(job_id)[0]
416 AssertEqual(len(result["handshake"]), 3)
417 AssertEqual(result["handshake"][0], constants.RIE_VERSION)
418 AssertEqual(len(result["x509_key_name"]), 3)
419 AssertIn("-----BEGIN CERTIFICATE-----", result["x509_ca"])
420
421
422 def TestNode(node):
423 """Testing getting node(s) info via remote API.
424
425 """
426 def _VerifyNode(data):
427 for entry in NODE_FIELDS:
428 AssertIn(entry, data)
429
430 def _VerifyNodesList(data):
431 for node in data:
432 for entry in LIST_FIELDS:
433 AssertIn(entry, node)
434
435 def _VerifyNodesBulk(data):
436 for node_data in data:
437 _VerifyNode(node_data)
438
439 _DoTests([
440 ("/2/nodes/%s" % node.primary, _VerifyNode, "GET", None),
441 ("/2/nodes", _VerifyNodesList, "GET", None),
442 ("/2/nodes?bulk=1", _VerifyNodesBulk, "GET", None),
443 ])
444
445
446 def _FilterTags(seq):
447 """Removes unwanted tags from a sequence.
448
449 """
450 ignore_re = qa_config.get("ignore-tags-re", None)
451
452 if ignore_re:
453 return itertools.ifilterfalse(re.compile(ignore_re).match, seq)
454 else:
455 return seq
456
457
458 def TestTags(kind, name, tags):
459 """Tests .../tags resources.
460
461 """
462 if kind == constants.TAG_CLUSTER:
463 uri = "/2/tags"
464 elif kind == constants.TAG_NODE:
465 uri = "/2/nodes/%s/tags" % name
466 elif kind == constants.TAG_INSTANCE:
467 uri = "/2/instances/%s/tags" % name
468 elif kind == constants.TAG_NODEGROUP:
469 uri = "/2/groups/%s/tags" % name
470 elif kind == constants.TAG_NETWORK:
471 uri = "/2/networks/%s/tags" % name
472 else:
473 raise errors.ProgrammerError("Unknown tag kind")
474
475 def _VerifyTags(data):
476 AssertEqual(sorted(tags), sorted(_FilterTags(data)))
477
478 queryargs = "&".join("tag=%s" % i for i in tags)
479
480 # Add tags
481 (job_id, ) = _DoTests([
482 ("%s?%s" % (uri, queryargs), _VerifyReturnsJob, "PUT", None),
483 ])
484 _WaitForRapiJob(job_id)
485
486 # Retrieve tags
487 _DoTests([
488 (uri, _VerifyTags, "GET", None),
489 ])
490
491 # Remove tags
492 (job_id, ) = _DoTests([
493 ("%s?%s" % (uri, queryargs), _VerifyReturnsJob, "DELETE", None),
494 ])
495 _WaitForRapiJob(job_id)
496
497
498 def _WaitForRapiJob(job_id):
499 """Waits for a job to finish.
500
501 """
502 def _VerifyJob(data):
503 AssertEqual(data["id"], job_id)
504 for field in JOB_FIELDS:
505 AssertIn(field, data)
506
507 _DoTests([
508 ("/2/jobs/%s" % job_id, _VerifyJob, "GET", None),
509 ])
510
511 return rapi.client_utils.PollJob(_rapi_client, job_id,
512 cli.StdioJobPollReportCb())
513
514
515 def TestRapiNodeGroups():
516 """Test several node group operations using RAPI.
517
518 """
519 (group1, group2, group3) = qa_utils.GetNonexistentGroups(3)
520
521 # Create a group with no attributes
522 body = {
523 "name": group1,
524 }
525
526 (job_id, ) = _DoTests([
527 ("/2/groups", _VerifyReturnsJob, "POST", body),
528 ])
529
530 _WaitForRapiJob(job_id)
531
532 # Create a group specifying alloc_policy
533 body = {
534 "name": group2,
535 "alloc_policy": constants.ALLOC_POLICY_UNALLOCABLE,
536 }
537
538 (job_id, ) = _DoTests([
539 ("/2/groups", _VerifyReturnsJob, "POST", body),
540 ])
541
542 _WaitForRapiJob(job_id)
543
544 # Modify alloc_policy
545 body = {
546 "alloc_policy": constants.ALLOC_POLICY_UNALLOCABLE,
547 }
548
549 (job_id, ) = _DoTests([
550 ("/2/groups/%s/modify" % group1, _VerifyReturnsJob, "PUT", body),
551 ])
552
553 _WaitForRapiJob(job_id)
554
555 # Rename a group
556 body = {
557 "new_name": group3,
558 }
559
560 (job_id, ) = _DoTests([
561 ("/2/groups/%s/rename" % group2, _VerifyReturnsJob, "PUT", body),
562 ])
563
564 _WaitForRapiJob(job_id)
565
566 # Delete groups
567 for group in [group1, group3]:
568 (job_id, ) = _DoTests([
569 ("/2/groups/%s" % group, _VerifyReturnsJob, "DELETE", None),
570 ])
571
572 _WaitForRapiJob(job_id)
573
574
575 def TestRapiInstanceAdd(node, use_client):
576 """Test adding a new instance via RAPI"""
577 if not qa_config.IsTemplateSupported(constants.DT_PLAIN):
578 return
579 instance = qa_config.AcquireInstance()
580 instance.SetDiskTemplate(constants.DT_PLAIN)
581 try:
582 disks = [{"size": utils.ParseUnit(d.get("size")),
583 "name": str(d.get("name"))}
584 for d in qa_config.GetDiskOptions()]
585 nic0_mac = instance.GetNicMacAddr(0, constants.VALUE_GENERATE)
586 nics = [{
587 constants.INIC_MAC: nic0_mac,
588 }]
589
590 beparams = {
591 constants.BE_MAXMEM: utils.ParseUnit(qa_config.get(constants.BE_MAXMEM)),
592 constants.BE_MINMEM: utils.ParseUnit(qa_config.get(constants.BE_MINMEM)),
593 }
594
595 if use_client:
596 job_id = _rapi_client.CreateInstance(constants.INSTANCE_CREATE,
597 instance.name,
598 constants.DT_PLAIN,
599 disks, nics,
600 os=qa_config.get("os"),
601 pnode=node.primary,
602 beparams=beparams)
603 else:
604 body = {
605 "__version__": 1,
606 "mode": constants.INSTANCE_CREATE,
607 "name": instance.name,
608 "os_type": qa_config.get("os"),
609 "disk_template": constants.DT_PLAIN,
610 "pnode": node.primary,
611 "beparams": beparams,
612 "disks": disks,
613 "nics": nics,
614 }
615
616 (job_id, ) = _DoTests([
617 ("/2/instances", _VerifyReturnsJob, "POST", body),
618 ])
619
620 _WaitForRapiJob(job_id)
621
622 return instance
623 except:
624 instance.Release()
625 raise
626
627
628 def _GenInstanceAllocationDict(node, instance):
629 """Creates an instance allocation dict to be used with the RAPI"""
630 instance.SetDiskTemplate(constants.DT_PLAIN)
631
632 disks = [{"size": utils.ParseUnit(d.get("size")),
633 "name": str(d.get("name"))}
634 for d in qa_config.GetDiskOptions()]
635
636 nic0_mac = instance.GetNicMacAddr(0, constants.VALUE_GENERATE)
637 nics = [{
638 constants.INIC_MAC: nic0_mac,
639 }]
640
641 beparams = {
642 constants.BE_MAXMEM: utils.ParseUnit(qa_config.get(constants.BE_MAXMEM)),
643 constants.BE_MINMEM: utils.ParseUnit(qa_config.get(constants.BE_MINMEM)),
644 }
645
646 return _rapi_client.InstanceAllocation(constants.INSTANCE_CREATE,
647 instance.name,
648 constants.DT_PLAIN,
649 disks, nics,
650 os=qa_config.get("os"),
651 pnode=node.primary,
652 beparams=beparams)
653
654
655 def TestRapiInstanceMultiAlloc(node):
656 """Test adding two new instances via the RAPI instance-multi-alloc method"""
657 if not qa_config.IsTemplateSupported(constants.DT_PLAIN):
658 return
659
660 JOBS_KEY = "jobs"
661
662 instance_one = qa_config.AcquireInstance()
663 instance_two = qa_config.AcquireInstance()
664 instance_list = [instance_one, instance_two]
665 try:
666 rapi_dicts = map(functools.partial(_GenInstanceAllocationDict, node),
667 instance_list)
668
669 job_id = _rapi_client.InstancesMultiAlloc(rapi_dicts)
670
671 results, = _WaitForRapiJob(job_id)
672
673 if JOBS_KEY not in results:
674 raise qa_error.Error("RAPI instance-multi-alloc did not deliver "
675 "information about created jobs")
676
677 if len(results[JOBS_KEY]) != len(instance_list):
678 raise qa_error.Error("RAPI instance-multi-alloc failed to return the "
679 "desired number of jobs!")
680
681 for success, job in results[JOBS_KEY]:
682 if success:
683 _WaitForRapiJob(job)
684 else:
685 raise qa_error.Error("Failed to create instance in "
686 "instance-multi-alloc call")
687 except:
688 # Note that although released, it may be that some of the instance creations
689 # have in fact succeeded. Handling this in a better way may be possible, but
690 # is not necessary as the QA has already failed at this point.
691 for instance in instance_list:
692 instance.Release()
693 raise
694
695 return (instance_one, instance_two)
696
697
698 @InstanceCheck(None, INST_DOWN, FIRST_ARG)
699 def TestRapiInstanceRemove(instance, use_client):
700 """Test removing instance via RAPI"""
701 # FIXME: this does not work if LVM is not enabled. Find out if this is a bug
702 # in RAPI or in the test
703 if not qa_config.IsStorageTypeSupported(constants.ST_LVM_VG):
704 return
705
706 if use_client:
707 job_id = _rapi_client.DeleteInstance(instance.name)
708 else:
709 (job_id, ) = _DoTests([
710 ("/2/instances/%s" % instance.name, _VerifyReturnsJob, "DELETE", None),
711 ])
712
713 _WaitForRapiJob(job_id)
714
715
716 @InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
717 def TestRapiInstanceMigrate(instance):
718 """Test migrating instance via RAPI"""
719 if not IsMigrationSupported(instance):
720 print qa_logging.FormatInfo("Instance doesn't support migration, skipping"
721 " test")
722 return
723 # Move to secondary node
724 _WaitForRapiJob(_rapi_client.MigrateInstance(instance.name))
725 qa_utils.RunInstanceCheck(instance, True)
726 # And back to previous primary
727 _WaitForRapiJob(_rapi_client.MigrateInstance(instance.name))
728
729
730 @InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
731 def TestRapiInstanceFailover(instance):
732 """Test failing over instance via RAPI"""
733 if not IsFailoverSupported(instance):
734 print qa_logging.FormatInfo("Instance doesn't support failover, skipping"
735 " test")
736 return
737 # Move to secondary node
738 _WaitForRapiJob(_rapi_client.FailoverInstance(instance.name))
739 qa_utils.RunInstanceCheck(instance, True)
740 # And back to previous primary
741 _WaitForRapiJob(_rapi_client.FailoverInstance(instance.name))
742
743
744 @InstanceCheck(INST_UP, INST_DOWN, FIRST_ARG)
745 def TestRapiInstanceShutdown(instance):
746 """Test stopping an instance via RAPI"""
747 _WaitForRapiJob(_rapi_client.ShutdownInstance(instance.name))
748
749
750 @InstanceCheck(INST_DOWN, INST_UP, FIRST_ARG)
751 def TestRapiInstanceStartup(instance):
752 """Test starting an instance via RAPI"""
753 _WaitForRapiJob(_rapi_client.StartupInstance(instance.name))
754
755
756 @InstanceCheck(INST_DOWN, INST_DOWN, FIRST_ARG)
757 def TestRapiInstanceRenameAndBack(rename_source, rename_target):
758 """Test renaming instance via RAPI
759
760 This must leave the instance with the original name (in the
761 non-failure case).
762
763 """
764 _WaitForRapiJob(_rapi_client.RenameInstance(rename_source, rename_target))
765 qa_utils.RunInstanceCheck(rename_source, False)
766 qa_utils.RunInstanceCheck(rename_target, False)
767 _WaitForRapiJob(_rapi_client.RenameInstance(rename_target, rename_source))
768 qa_utils.RunInstanceCheck(rename_target, False)
769
770
771 @InstanceCheck(INST_DOWN, INST_DOWN, FIRST_ARG)
772 def TestRapiInstanceReinstall(instance):
773 """Test reinstalling an instance via RAPI"""
774 if instance.disk_template == constants.DT_DISKLESS:
775 print qa_logging.FormatInfo("Test not supported for diskless instances")
776 return
777
778 _WaitForRapiJob(_rapi_client.ReinstallInstance(instance.name))
779 # By default, the instance is started again
780 qa_utils.RunInstanceCheck(instance, True)
781
782 # Reinstall again without starting
783 _WaitForRapiJob(_rapi_client.ReinstallInstance(instance.name,
784 no_startup=True))
785
786
787 @InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
788 def TestRapiInstanceReplaceDisks(instance):
789 """Test replacing instance disks via RAPI"""
790 if not IsDiskReplacingSupported(instance):
791 print qa_logging.FormatInfo("Instance doesn't support disk replacing,"
792 " skipping test")
793 return
794 fn = _rapi_client.ReplaceInstanceDisks
795 _WaitForRapiJob(fn(instance.name,
796 mode=constants.REPLACE_DISK_AUTO, disks=[]))
797 _WaitForRapiJob(fn(instance.name,
798 mode=constants.REPLACE_DISK_SEC, disks="0"))
799
800
801 @InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
802 def TestRapiInstanceModify(instance):
803 """Test modifying instance via RAPI"""
804 default_hv = qa_config.GetDefaultHypervisor()
805
806 def _ModifyInstance(**kwargs):
807 _WaitForRapiJob(_rapi_client.ModifyInstance(instance.name, **kwargs))
808
809 _ModifyInstance(beparams={
810 constants.BE_VCPUS: 3,
811 })
812
813 _ModifyInstance(beparams={
814 constants.BE_VCPUS: constants.VALUE_DEFAULT,
815 })
816
817 if default_hv == constants.HT_XEN_PVM:
818 _ModifyInstance(hvparams={
819 constants.HV_KERNEL_ARGS: "single",
820 })
821 _ModifyInstance(hvparams={
822 constants.HV_KERNEL_ARGS: constants.VALUE_DEFAULT,
823 })
824 elif default_hv == constants.HT_XEN_HVM:
825 _ModifyInstance(hvparams={
826 constants.HV_BOOT_ORDER: "acn",
827 })
828 _ModifyInstance(hvparams={
829 constants.HV_BOOT_ORDER: constants.VALUE_DEFAULT,
830 })
831
832
833 @InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
834 def TestRapiInstanceConsole(instance):
835 """Test getting instance console information via RAPI"""
836 result = _rapi_client.GetInstanceConsole(instance.name)
837 console = objects.InstanceConsole.FromDict(result)
838 AssertEqual(console.Validate(), True)
839 AssertEqual(console.instance, qa_utils.ResolveInstanceName(instance.name))
840
841
842 @InstanceCheck(INST_DOWN, INST_DOWN, FIRST_ARG)
843 def TestRapiStoppedInstanceConsole(instance):
844 """Test getting stopped instance's console information via RAPI"""
845 try:
846 _rapi_client.GetInstanceConsole(instance.name)
847 except rapi.client.GanetiApiError, err:
848 AssertEqual(err.code, 503)
849 else:
850 raise qa_error.Error("Getting console for stopped instance didn't"
851 " return HTTP 503")
852
853
854 def GetOperatingSystems():
855 """Retrieves a list of all available operating systems.
856
857 """
858 return _rapi_client.GetOperatingSystems()
859
860
861 def TestInterClusterInstanceMove(src_instance, dest_instance,
862 inodes, tnode):
863 """Test tools/move-instance"""
864 master = qa_config.GetMasterNode()
865
866 rapi_pw_file = tempfile.NamedTemporaryFile()
867 rapi_pw_file.write(_rapi_password)
868 rapi_pw_file.flush()
869
870 dest_instance.SetDiskTemplate(src_instance.disk_template)
871
872 # TODO: Run some instance tests before moving back
873
874 if len(inodes) > 1:
875 # No disk template currently requires more than 1 secondary node. If this
876 # changes, either this test must be skipped or the script must be updated.
877 assert len(inodes) == 2
878 snode = inodes[1]
879 else:
880 # instance is not redundant, but we still need to pass a node
881 # (which will be ignored)
882 snode = tnode
883 pnode = inodes[0]
884 # note: pnode:snode are the *current* nodes, so we move it first to
885 # tnode:pnode, then back to pnode:snode
886 for si, di, pn, sn in [(src_instance.name, dest_instance.name,
887 tnode.primary, pnode.primary),
888 (dest_instance.name, src_instance.name,
889 pnode.primary, snode.primary)]:
890 cmd = [
891 "../tools/move-instance",
892 "--verbose",
893 "--src-ca-file=%s" % _rapi_ca.name,
894 "--src-username=%s" % _rapi_username,
895 "--src-password-file=%s" % rapi_pw_file.name,
896 "--dest-instance-name=%s" % di,
897 "--dest-primary-node=%s" % pn,
898 "--dest-secondary-node=%s" % sn,
899 "--net=0:mac=%s" % constants.VALUE_GENERATE,
900 master.primary,
901 master.primary,
902 si,
903 ]
904
905 qa_utils.RunInstanceCheck(di, False)
906 AssertEqual(StartLocalCommand(cmd).wait(), 0)
907 qa_utils.RunInstanceCheck(si, False)
908 qa_utils.RunInstanceCheck(di, True)
909
910
911 _DRBD_SECRET_RE = re.compile('shared-secret.*"([0-9A-Fa-f]+)"')
912
913
914 def _RetrieveSecret(instance, pnode):
915 """Retrieves the DRBD secret given an instance object and the primary node.
916
917 @type instance: L{qa_config._QaInstance}
918 @type pnode: L{qa_config._QaNode}
919
920 @rtype: string
921
922 """
923 instance_info = GetInstanceInfo(instance.name)
924
925 # We are interested in only the first disk on the primary
926 drbd_minor = instance_info["drbd-minors"][pnode.primary][0]
927
928 # This form should work for all DRBD versions
929 drbd_command = ("drbdsetup show %d; drbdsetup %d show || true" %
930 (drbd_minor, drbd_minor))
931 instance_drbd_info = \
932 qa_utils.GetCommandOutput(pnode.primary, drbd_command)
933
934 match_obj = _DRBD_SECRET_RE.search(instance_drbd_info)
935 if match_obj is None:
936 raise qa_error.Error("Could not retrieve DRBD secret for instance %s from"
937 " node %s." % (instance.name, pnode.primary))
938
939 return match_obj.groups(0)[0]
940
941
942 def TestInstanceDataCensorship(instance, inodes):
943 """Test protection of sensitive instance data."""
944
945 if instance.disk_template != constants.DT_DRBD8:
946 print qa_utils.FormatInfo("Only the DRBD secret is a sensitive parameter"
947 " right now, skipping for non-DRBD instance.")
948 return
949
950 drbd_secret = _RetrieveSecret(instance, inodes[0])
951
952 job_id = _rapi_client.GetInstanceInfo(instance.name)
953 if not _rapi_client.WaitForJobCompletion(job_id):
954 raise qa_error.Error("Could not fetch instance info for instance %s" %
955 instance.name)
956 info_dict = _rapi_client.GetJobStatus(job_id)
957
958 if drbd_secret in str(info_dict):
959 print qa_utils.FormatInfo("DRBD secret: %s" % drbd_secret)
960 print qa_utils.FormatInfo("Retrieved data\n%s" % str(info_dict))
961 raise qa_error.Error("Found DRBD secret in contents of RAPI instance info"
962 " call; see above.")