Merge branch 'stable-2.10' into stable-2.11
[ganeti-github.git] / qa / qa_rapi.py
1 #
2 #
3
4 # Copyright (C) 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 Google Inc.
5 # All rights reserved.
6 #
7 # Redistribution and use in source and binary forms, with or without
8 # modification, are permitted provided that the following conditions are
9 # met:
10 #
11 # 1. Redistributions of source code must retain the above copyright notice,
12 # this list of conditions and the following disclaimer.
13 #
14 # 2. Redistributions in binary form must reproduce the above copyright
15 # notice, this list of conditions and the following disclaimer in the
16 # documentation and/or other materials provided with the distribution.
17 #
18 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
19 # IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
20 # TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
21 # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
22 # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
23 # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
24 # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
25 # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
26 # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
27 # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
28 # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29
30
31 """Remote API QA tests.
32
33 """
34
35 import functools
36 import itertools
37 import random
38 import re
39 import tempfile
40
41 from ganeti import cli
42 from ganeti import compat
43 from ganeti import constants
44 from ganeti import errors
45 from ganeti import pathutils
46 from ganeti import objects
47 from ganeti import opcodes
48 from ganeti import query
49 from ganeti import qlang
50 from ganeti import rapi
51 from ganeti import utils
52
53 from ganeti.http.auth import ParsePasswordFile
54 import ganeti.rapi.client # pylint: disable=W0611
55 import ganeti.rapi.client_utils
56
57 import qa_config
58 import qa_error
59 import qa_logging
60 import qa_utils
61
62 from qa_instance import GetInstanceInfo
63 from qa_instance import IsFailoverSupported
64 from qa_instance import IsMigrationSupported
65 from qa_instance import IsDiskReplacingSupported
66 from qa_utils import (AssertEqual, AssertIn, AssertMatch, StartLocalCommand)
67 from qa_utils import InstanceCheck, INST_DOWN, INST_UP, FIRST_ARG
68
69
70 _rapi_ca = None
71 _rapi_client = None
72 _rapi_username = None
73 _rapi_password = None
74
75
76 def Setup(username, password):
77 """Configures the RAPI client.
78
79 """
80 # pylint: disable=W0603
81 # due to global usage
82 global _rapi_ca
83 global _rapi_client
84 global _rapi_username
85 global _rapi_password
86
87 _rapi_username = username
88 _rapi_password = password
89
90 master = qa_config.GetMasterNode()
91
92 # Load RAPI certificate from master node
93 cmd = ["cat", qa_utils.MakeNodePath(master, pathutils.RAPI_CERT_FILE)]
94
95 # Write to temporary file
96 _rapi_ca = tempfile.NamedTemporaryFile()
97 _rapi_ca.write(qa_utils.GetCommandOutput(master.primary,
98 utils.ShellQuoteArgs(cmd)))
99 _rapi_ca.flush()
100
101 port = qa_config.get("rapi-port", default=constants.DEFAULT_RAPI_PORT)
102 cfg_curl = rapi.client.GenericCurlConfig(cafile=_rapi_ca.name,
103 proxy="")
104
105 if qa_config.UseVirtualCluster():
106 # TODO: Implement full support for RAPI on virtual clusters
107 print qa_logging.FormatWarning("RAPI tests are not yet supported on"
108 " virtual clusters and will be disabled")
109
110 assert _rapi_client is None
111 else:
112 _rapi_client = rapi.client.GanetiRapiClient(master.primary, port=port,
113 username=username,
114 password=password,
115 curl_config_fn=cfg_curl)
116
117 print "RAPI protocol version: %s" % _rapi_client.GetVersion()
118
119 return _rapi_client
120
121
122 def LookupRapiSecret(rapi_user):
123 """Find the RAPI secret for the given user.
124
125 @param rapi_user: Login user
126 @return: Login secret for the user
127
128 """
129 CTEXT = "{CLEARTEXT}"
130 master = qa_config.GetMasterNode()
131 cmd = ["cat", qa_utils.MakeNodePath(master, pathutils.RAPI_USERS_FILE)]
132 file_content = qa_utils.GetCommandOutput(master.primary,
133 utils.ShellQuoteArgs(cmd))
134 users = ParsePasswordFile(file_content)
135 entry = users.get(rapi_user)
136 if not entry:
137 raise qa_error.Error("User %s not found in RAPI users file" % rapi_user)
138 secret = entry.password
139 if secret.upper().startswith(CTEXT):
140 secret = secret[len(CTEXT):]
141 elif secret.startswith("{"):
142 raise qa_error.Error("Unsupported password schema for RAPI user %s:"
143 " not a clear text password" % rapi_user)
144 return secret
145
146
147 INSTANCE_FIELDS = ("name", "os", "pnode", "snodes",
148 "admin_state",
149 "disk_template", "disk.sizes", "disk.spindles",
150 "nic.ips", "nic.macs", "nic.modes", "nic.links",
151 "beparams", "hvparams",
152 "oper_state", "oper_ram", "oper_vcpus", "status", "tags")
153
154 NODE_FIELDS = ("name", "dtotal", "dfree", "sptotal", "spfree",
155 "mtotal", "mnode", "mfree",
156 "pinst_cnt", "sinst_cnt", "tags")
157
158 GROUP_FIELDS = compat.UniqueFrozenset([
159 "name", "uuid",
160 "alloc_policy",
161 "node_cnt", "node_list",
162 ])
163
164 JOB_FIELDS = compat.UniqueFrozenset([
165 "id", "ops", "status", "summary",
166 "opstatus", "opresult", "oplog",
167 "received_ts", "start_ts", "end_ts",
168 ])
169
170 LIST_FIELDS = ("id", "uri")
171
172
173 def Enabled():
174 """Return whether remote API tests should be run.
175
176 """
177 # TODO: Implement RAPI tests for virtual clusters
178 return (qa_config.TestEnabled("rapi") and
179 not qa_config.UseVirtualCluster())
180
181
182 def _DoTests(uris):
183 # pylint: disable=W0212
184 # due to _SendRequest usage
185 results = []
186
187 for uri, verify, method, body in uris:
188 assert uri.startswith("/")
189
190 print "%s %s" % (method, uri)
191 data = _rapi_client._SendRequest(method, uri, None, body)
192
193 if verify is not None:
194 if callable(verify):
195 verify(data)
196 else:
197 AssertEqual(data, verify)
198
199 results.append(data)
200
201 return results
202
203
204 # pylint: disable=W0212
205 # Due to _SendRequest usage
206 def _DoGetPutTests(get_uri, modify_uri, opcode_params, rapi_only_aliases=None,
207 modify_method="PUT", exceptions=None, set_exceptions=None):
208 """ Test if all params of an object can be retrieved, and set as well.
209
210 @type get_uri: string
211 @param get_uri: The URI from which information about the object can be
212 retrieved.
213 @type modify_uri: string
214 @param modify_uri: The URI which can be used to modify the object.
215 @type opcode_params: list of tuple
216 @param opcode_params: The parameters of the underlying opcode, used to
217 determine which parameters are actually present.
218 @type rapi_only_aliases: list of string or None
219 @param rapi_only_aliases: Aliases for parameters which differ from the opcode,
220 and become renamed before opcode submission.
221 @type modify_method: string
222 @param modify_method: The method to be used in the modification.
223 @type exceptions: list of string or None
224 @param exceptions: The parameters which have not been exposed and should not
225 be tested at all.
226 @type set_exceptions: list of string or None
227 @param set_exceptions: The parameters whose setting should not be tested as a
228 part of this test.
229
230 """
231
232 assert get_uri.startswith("/")
233 assert modify_uri.startswith("/")
234
235 if exceptions is None:
236 exceptions = []
237 if set_exceptions is None:
238 set_exceptions = []
239
240 print "Testing get/modify symmetry of %s and %s" % (get_uri, modify_uri)
241
242 # First we see if all parameters of the opcode are returned through RAPI
243 params_of_interest = map(lambda x: x[0], opcode_params)
244
245 # The RAPI-specific aliases are to be checked as well
246 if rapi_only_aliases is not None:
247 params_of_interest.extend(rapi_only_aliases)
248
249 info = _rapi_client._SendRequest("GET", get_uri, None, {})
250
251 missing_params = filter(lambda x: x not in info and x not in exceptions,
252 params_of_interest)
253 if missing_params:
254 raise qa_error.Error("The parameters %s which can be set through the "
255 "appropriate opcode are not present in the response "
256 "from %s" % (','.join(missing_params), get_uri))
257
258 print "GET successful at %s" % get_uri
259
260 # Then if we can perform a set with the same values as received
261 put_payload = {}
262 for param in params_of_interest:
263 if param not in exceptions and param not in set_exceptions:
264 put_payload[param] = info[param]
265
266 _rapi_client._SendRequest(modify_method, modify_uri, None, put_payload)
267
268 print "%s successful at %s" % (modify_method, modify_uri)
269 # pylint: enable=W0212
270
271
272 def _VerifyReturnsJob(data):
273 if not isinstance(data, int):
274 AssertMatch(data, r"^\d+$")
275
276
277 def TestVersion():
278 """Testing remote API version.
279
280 """
281 _DoTests([
282 ("/version", constants.RAPI_VERSION, "GET", None),
283 ])
284
285
286 def TestEmptyCluster():
287 """Testing remote API on an empty cluster.
288
289 """
290 master = qa_config.GetMasterNode()
291 master_full = qa_utils.ResolveNodeName(master)
292
293 def _VerifyInfo(data):
294 AssertIn("name", data)
295 AssertIn("master", data)
296 AssertEqual(data["master"], master_full)
297
298 def _VerifyNodes(data):
299 master_entry = {
300 "id": master_full,
301 "uri": "/2/nodes/%s" % master_full,
302 }
303 AssertIn(master_entry, data)
304
305 def _VerifyNodesBulk(data):
306 for node in data:
307 for entry in NODE_FIELDS:
308 AssertIn(entry, node)
309
310 def _VerifyGroups(data):
311 default_group = {
312 "name": constants.INITIAL_NODE_GROUP_NAME,
313 "uri": "/2/groups/" + constants.INITIAL_NODE_GROUP_NAME,
314 }
315 AssertIn(default_group, data)
316
317 def _VerifyGroupsBulk(data):
318 for group in data:
319 for field in GROUP_FIELDS:
320 AssertIn(field, group)
321
322 _DoTests([
323 ("/", None, "GET", None),
324 ("/2/info", _VerifyInfo, "GET", None),
325 ("/2/tags", None, "GET", None),
326 ("/2/nodes", _VerifyNodes, "GET", None),
327 ("/2/nodes?bulk=1", _VerifyNodesBulk, "GET", None),
328 ("/2/groups", _VerifyGroups, "GET", None),
329 ("/2/groups?bulk=1", _VerifyGroupsBulk, "GET", None),
330 ("/2/instances", [], "GET", None),
331 ("/2/instances?bulk=1", [], "GET", None),
332 ("/2/os", None, "GET", None),
333 ])
334
335 # Test HTTP Not Found
336 for method in ["GET", "PUT", "POST", "DELETE"]:
337 try:
338 _DoTests([("/99/resource/not/here/99", None, method, None)])
339 except rapi.client.GanetiApiError, err:
340 AssertEqual(err.code, 404)
341 else:
342 raise qa_error.Error("Non-existent resource didn't return HTTP 404")
343
344 # Test HTTP Not Implemented
345 for method in ["PUT", "POST", "DELETE"]:
346 try:
347 _DoTests([("/version", None, method, None)])
348 except rapi.client.GanetiApiError, err:
349 AssertEqual(err.code, 501)
350 else:
351 raise qa_error.Error("Non-implemented method didn't fail")
352
353 # Test GET/PUT symmetry
354 LEGITIMATELY_MISSING = [
355 "force", # Standard option
356 "add_uids", # Modifies UID pool, is not a param itself
357 "remove_uids", # Same as above
358 ]
359 NOT_EXPOSED_YET = ["hv_state", "disk_state", "modify_etc_hosts"]
360 # The nicparams are returned under the default entry, yet accepted as they
361 # are - this is a TODO to fix!
362 DEFAULT_ISSUES = ["nicparams"]
363
364 _DoGetPutTests("/2/info", "/2/modify", opcodes.OpClusterSetParams.OP_PARAMS,
365 exceptions=(LEGITIMATELY_MISSING + NOT_EXPOSED_YET),
366 set_exceptions=DEFAULT_ISSUES)
367
368
369 def TestRapiQuery():
370 """Testing resource queries via remote API.
371
372 """
373 # FIXME: the tests are failing if no LVM is enabled, investigate
374 # if it is a bug in the QA or in the code
375 if not qa_config.IsStorageTypeSupported(constants.ST_LVM_VG):
376 return
377
378 master_name = qa_utils.ResolveNodeName(qa_config.GetMasterNode())
379 rnd = random.Random(7818)
380
381 for what in constants.QR_VIA_RAPI:
382 if what == constants.QR_JOB:
383 namefield = "id"
384 trivial_filter = [qlang.OP_GE, namefield, 0]
385 elif what == constants.QR_EXPORT:
386 namefield = "export"
387 trivial_filter = [qlang.OP_REGEXP, ".*", namefield]
388 else:
389 namefield = "name"
390 trivial_filter = [qlang.OP_REGEXP, ".*", namefield]
391
392 all_fields = query.ALL_FIELDS[what].keys()
393 rnd.shuffle(all_fields)
394
395 # No fields, should return everything
396 result = _rapi_client.QueryFields(what)
397 qresult = objects.QueryFieldsResponse.FromDict(result)
398 AssertEqual(len(qresult.fields), len(all_fields))
399
400 # One field
401 result = _rapi_client.QueryFields(what, fields=[namefield])
402 qresult = objects.QueryFieldsResponse.FromDict(result)
403 AssertEqual(len(qresult.fields), 1)
404
405 # Specify all fields, order must be correct
406 result = _rapi_client.QueryFields(what, fields=all_fields)
407 qresult = objects.QueryFieldsResponse.FromDict(result)
408 AssertEqual(len(qresult.fields), len(all_fields))
409 AssertEqual([fdef.name for fdef in qresult.fields], all_fields)
410
411 # Unknown field
412 result = _rapi_client.QueryFields(what, fields=["_unknown!"])
413 qresult = objects.QueryFieldsResponse.FromDict(result)
414 AssertEqual(len(qresult.fields), 1)
415 AssertEqual(qresult.fields[0].name, "_unknown!")
416 AssertEqual(qresult.fields[0].kind, constants.QFT_UNKNOWN)
417
418 # Try once more, this time without the client
419 _DoTests([
420 ("/2/query/%s/fields" % what, None, "GET", None),
421 ("/2/query/%s/fields?fields=name,name,%s" % (what, all_fields[0]),
422 None, "GET", None),
423 ])
424
425 # Try missing query argument
426 try:
427 _DoTests([
428 ("/2/query/%s" % what, None, "GET", None),
429 ])
430 except rapi.client.GanetiApiError, err:
431 AssertEqual(err.code, 400)
432 else:
433 raise qa_error.Error("Request missing 'fields' parameter didn't fail")
434
435 def _Check(exp_fields, data):
436 qresult = objects.QueryResponse.FromDict(data)
437 AssertEqual([fdef.name for fdef in qresult.fields], exp_fields)
438 if not isinstance(qresult.data, list):
439 raise qa_error.Error("Query did not return a list")
440
441 _DoTests([
442 # Specify fields in query
443 ("/2/query/%s?fields=%s" % (what, ",".join(all_fields)),
444 compat.partial(_Check, all_fields), "GET", None),
445
446 ("/2/query/%s?fields=%s" % (what, namefield),
447 compat.partial(_Check, [namefield]), "GET", None),
448
449 # Note the spaces
450 ("/2/query/%s?fields=%s,%%20%s%%09,%s%%20" %
451 (what, namefield, namefield, namefield),
452 compat.partial(_Check, [namefield] * 3), "GET", None)])
453
454 if what in constants.QR_VIA_RAPI_PUT:
455 _DoTests([
456 # PUT with fields in query
457 ("/2/query/%s?fields=%s" % (what, namefield),
458 compat.partial(_Check, [namefield]), "PUT", {}),
459
460 ("/2/query/%s" % what, compat.partial(_Check, [namefield] * 4), "PUT", {
461 "fields": [namefield] * 4,
462 }),
463
464 ("/2/query/%s" % what, compat.partial(_Check, all_fields), "PUT", {
465 "fields": all_fields,
466 }),
467
468 ("/2/query/%s" % what, compat.partial(_Check, [namefield] * 4), "PUT", {
469 "fields": [namefield] * 4
470 })])
471
472 def _CheckFilter():
473 _DoTests([
474 # With filter
475 ("/2/query/%s" % what, compat.partial(_Check, all_fields), "PUT", {
476 "fields": all_fields,
477 "filter": trivial_filter
478 }),
479 ])
480
481 if what == constants.QR_LOCK:
482 # Locks can't be filtered
483 try:
484 _CheckFilter()
485 except rapi.client.GanetiApiError, err:
486 AssertEqual(err.code, 500)
487 else:
488 raise qa_error.Error("Filtering locks didn't fail")
489 else:
490 if what in constants.QR_VIA_RAPI_PUT:
491 _CheckFilter()
492
493 if what == constants.QR_NODE:
494 # Test with filter
495 (nodes, ) = _DoTests(
496 [("/2/query/%s" % what,
497 compat.partial(_Check, ["name", "master"]), "PUT",
498 {"fields": ["name", "master"],
499 "filter": [qlang.OP_TRUE, "master"],
500 })])
501 qresult = objects.QueryResponse.FromDict(nodes)
502 AssertEqual(qresult.data, [
503 [[constants.RS_NORMAL, master_name], [constants.RS_NORMAL, True]],
504 ])
505
506
507 @InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
508 def TestInstance(instance):
509 """Testing getting instance(s) info via remote API.
510
511 """
512 def _VerifyInstance(data):
513 for entry in INSTANCE_FIELDS:
514 AssertIn(entry, data)
515
516 def _VerifyInstancesList(data):
517 for instance in data:
518 for entry in LIST_FIELDS:
519 AssertIn(entry, instance)
520
521 def _VerifyInstancesBulk(data):
522 for instance_data in data:
523 _VerifyInstance(instance_data)
524
525 _DoTests([
526 ("/2/instances/%s" % instance.name, _VerifyInstance, "GET", None),
527 ("/2/instances", _VerifyInstancesList, "GET", None),
528 ("/2/instances?bulk=1", _VerifyInstancesBulk, "GET", None),
529 ("/2/instances/%s/activate-disks" % instance.name,
530 _VerifyReturnsJob, "PUT", None),
531 ("/2/instances/%s/deactivate-disks" % instance.name,
532 _VerifyReturnsJob, "PUT", None),
533 ])
534
535 # Test OpBackupPrepare
536 (job_id, ) = _DoTests([
537 ("/2/instances/%s/prepare-export?mode=%s" %
538 (instance.name, constants.EXPORT_MODE_REMOTE),
539 _VerifyReturnsJob, "PUT", None),
540 ])
541
542 result = _WaitForRapiJob(job_id)[0]
543 AssertEqual(len(result["handshake"]), 3)
544 AssertEqual(result["handshake"][0], constants.RIE_VERSION)
545 AssertEqual(len(result["x509_key_name"]), 3)
546 AssertIn("-----BEGIN CERTIFICATE-----", result["x509_ca"])
547
548
549 def TestNode(node):
550 """Testing getting node(s) info via remote API.
551
552 """
553 def _VerifyNode(data):
554 for entry in NODE_FIELDS:
555 AssertIn(entry, data)
556
557 def _VerifyNodesList(data):
558 for node in data:
559 for entry in LIST_FIELDS:
560 AssertIn(entry, node)
561
562 def _VerifyNodesBulk(data):
563 for node_data in data:
564 _VerifyNode(node_data)
565
566 _DoTests([
567 ("/2/nodes/%s" % node.primary, _VerifyNode, "GET", None),
568 ("/2/nodes", _VerifyNodesList, "GET", None),
569 ("/2/nodes?bulk=1", _VerifyNodesBulk, "GET", None),
570 ])
571
572 # Not parameters of the node, but controlling opcode behavior
573 LEGITIMATELY_MISSING = ["force", "powered"]
574 # Identifying the node - RAPI provides these itself
575 IDENTIFIERS = ["node_name", "node_uuid"]
576 # As the name states, these can be set but not retrieved yet
577 NOT_EXPOSED_YET = ["hv_state", "disk_state", "auto_promote"]
578
579 _DoGetPutTests("/2/nodes/%s" % node.primary,
580 "/2/nodes/%s/modify" % node.primary,
581 opcodes.OpNodeSetParams.OP_PARAMS,
582 modify_method="POST",
583 exceptions=(LEGITIMATELY_MISSING + NOT_EXPOSED_YET +
584 IDENTIFIERS))
585
586
587 def _FilterTags(seq):
588 """Removes unwanted tags from a sequence.
589
590 """
591 ignore_re = qa_config.get("ignore-tags-re", None)
592
593 if ignore_re:
594 return itertools.ifilterfalse(re.compile(ignore_re).match, seq)
595 else:
596 return seq
597
598
599 def TestTags(kind, name, tags):
600 """Tests .../tags resources.
601
602 """
603 if kind == constants.TAG_CLUSTER:
604 uri = "/2/tags"
605 elif kind == constants.TAG_NODE:
606 uri = "/2/nodes/%s/tags" % name
607 elif kind == constants.TAG_INSTANCE:
608 uri = "/2/instances/%s/tags" % name
609 elif kind == constants.TAG_NODEGROUP:
610 uri = "/2/groups/%s/tags" % name
611 elif kind == constants.TAG_NETWORK:
612 uri = "/2/networks/%s/tags" % name
613 else:
614 raise errors.ProgrammerError("Unknown tag kind")
615
616 def _VerifyTags(data):
617 AssertEqual(sorted(tags), sorted(_FilterTags(data)))
618
619 queryargs = "&".join("tag=%s" % i for i in tags)
620
621 # Add tags
622 (job_id, ) = _DoTests([
623 ("%s?%s" % (uri, queryargs), _VerifyReturnsJob, "PUT", None),
624 ])
625 _WaitForRapiJob(job_id)
626
627 # Retrieve tags
628 _DoTests([
629 (uri, _VerifyTags, "GET", None),
630 ])
631
632 # Remove tags
633 (job_id, ) = _DoTests([
634 ("%s?%s" % (uri, queryargs), _VerifyReturnsJob, "DELETE", None),
635 ])
636 _WaitForRapiJob(job_id)
637
638
639 def _WaitForRapiJob(job_id):
640 """Waits for a job to finish.
641
642 """
643 def _VerifyJob(data):
644 AssertEqual(data["id"], job_id)
645 for field in JOB_FIELDS:
646 AssertIn(field, data)
647
648 _DoTests([
649 ("/2/jobs/%s" % job_id, _VerifyJob, "GET", None),
650 ])
651
652 return rapi.client_utils.PollJob(_rapi_client, job_id,
653 cli.StdioJobPollReportCb())
654
655
656 def TestRapiNodeGroups():
657 """Test several node group operations using RAPI.
658
659 """
660 (group1, group2, group3) = qa_utils.GetNonexistentGroups(3)
661
662 # Create a group with no attributes
663 body = {
664 "name": group1,
665 }
666
667 (job_id, ) = _DoTests([
668 ("/2/groups", _VerifyReturnsJob, "POST", body),
669 ])
670
671 _WaitForRapiJob(job_id)
672
673 # Create a group specifying alloc_policy
674 body = {
675 "name": group2,
676 "alloc_policy": constants.ALLOC_POLICY_UNALLOCABLE,
677 }
678
679 (job_id, ) = _DoTests([
680 ("/2/groups", _VerifyReturnsJob, "POST", body),
681 ])
682
683 _WaitForRapiJob(job_id)
684
685 # Modify alloc_policy
686 body = {
687 "alloc_policy": constants.ALLOC_POLICY_UNALLOCABLE,
688 }
689
690 (job_id, ) = _DoTests([
691 ("/2/groups/%s/modify" % group1, _VerifyReturnsJob, "PUT", body),
692 ])
693
694 _WaitForRapiJob(job_id)
695
696 # Rename a group
697 body = {
698 "new_name": group3,
699 }
700
701 (job_id, ) = _DoTests([
702 ("/2/groups/%s/rename" % group2, _VerifyReturnsJob, "PUT", body),
703 ])
704
705 _WaitForRapiJob(job_id)
706
707 # Test for get/set symmetry
708
709 # Identifying the node - RAPI provides these itself
710 IDENTIFIERS = ["group_name"]
711 # As the name states, not exposed yet
712 NOT_EXPOSED_YET = ["hv_state", "disk_state"]
713
714 # The parameters we do not want to get and set (as that sets the
715 # group-specific params to the filled ones)
716 FILLED_PARAMS = ["ndparams", "ipolicy", "diskparams"]
717
718 # The aliases that we can use to perform this test with the group-specific
719 # params
720 CUSTOM_PARAMS = ["custom_ndparams", "custom_ipolicy", "custom_diskparams"]
721
722 _DoGetPutTests("/2/groups/%s" % group3, "/2/groups/%s/modify" % group3,
723 opcodes.OpGroupSetParams.OP_PARAMS,
724 rapi_only_aliases=CUSTOM_PARAMS,
725 exceptions=(IDENTIFIERS + NOT_EXPOSED_YET),
726 set_exceptions=FILLED_PARAMS)
727
728 # Delete groups
729 for group in [group1, group3]:
730 (job_id, ) = _DoTests([
731 ("/2/groups/%s" % group, _VerifyReturnsJob, "DELETE", None),
732 ])
733
734 _WaitForRapiJob(job_id)
735
736
737 def TestRapiInstanceAdd(node, use_client):
738 """Test adding a new instance via RAPI"""
739 if not qa_config.IsTemplateSupported(constants.DT_PLAIN):
740 return
741 instance = qa_config.AcquireInstance()
742 instance.SetDiskTemplate(constants.DT_PLAIN)
743 try:
744 disks = [{"size": utils.ParseUnit(d.get("size")),
745 "name": str(d.get("name"))}
746 for d in qa_config.GetDiskOptions()]
747 nic0_mac = instance.GetNicMacAddr(0, constants.VALUE_GENERATE)
748 nics = [{
749 constants.INIC_MAC: nic0_mac,
750 }]
751
752 beparams = {
753 constants.BE_MAXMEM: utils.ParseUnit(qa_config.get(constants.BE_MAXMEM)),
754 constants.BE_MINMEM: utils.ParseUnit(qa_config.get(constants.BE_MINMEM)),
755 }
756
757 if use_client:
758 job_id = _rapi_client.CreateInstance(constants.INSTANCE_CREATE,
759 instance.name,
760 constants.DT_PLAIN,
761 disks, nics,
762 os=qa_config.get("os"),
763 pnode=node.primary,
764 beparams=beparams)
765 else:
766 body = {
767 "__version__": 1,
768 "mode": constants.INSTANCE_CREATE,
769 "name": instance.name,
770 "os_type": qa_config.get("os"),
771 "disk_template": constants.DT_PLAIN,
772 "pnode": node.primary,
773 "beparams": beparams,
774 "disks": disks,
775 "nics": nics,
776 }
777
778 (job_id, ) = _DoTests([
779 ("/2/instances", _VerifyReturnsJob, "POST", body),
780 ])
781
782 _WaitForRapiJob(job_id)
783
784 return instance
785 except:
786 instance.Release()
787 raise
788
789
790 def _GenInstanceAllocationDict(node, instance):
791 """Creates an instance allocation dict to be used with the RAPI"""
792 instance.SetDiskTemplate(constants.DT_PLAIN)
793
794 disks = [{"size": utils.ParseUnit(d.get("size")),
795 "name": str(d.get("name"))}
796 for d in qa_config.GetDiskOptions()]
797
798 nic0_mac = instance.GetNicMacAddr(0, constants.VALUE_GENERATE)
799 nics = [{
800 constants.INIC_MAC: nic0_mac,
801 }]
802
803 beparams = {
804 constants.BE_MAXMEM: utils.ParseUnit(qa_config.get(constants.BE_MAXMEM)),
805 constants.BE_MINMEM: utils.ParseUnit(qa_config.get(constants.BE_MINMEM)),
806 }
807
808 return _rapi_client.InstanceAllocation(constants.INSTANCE_CREATE,
809 instance.name,
810 constants.DT_PLAIN,
811 disks, nics,
812 os=qa_config.get("os"),
813 pnode=node.primary,
814 beparams=beparams)
815
816
817 def TestRapiInstanceMultiAlloc(node):
818 """Test adding two new instances via the RAPI instance-multi-alloc method"""
819 if not qa_config.IsTemplateSupported(constants.DT_PLAIN):
820 return
821
822 JOBS_KEY = "jobs"
823
824 instance_one = qa_config.AcquireInstance()
825 instance_two = qa_config.AcquireInstance()
826 instance_list = [instance_one, instance_two]
827 try:
828 rapi_dicts = map(functools.partial(_GenInstanceAllocationDict, node),
829 instance_list)
830
831 job_id = _rapi_client.InstancesMultiAlloc(rapi_dicts)
832
833 results, = _WaitForRapiJob(job_id)
834
835 if JOBS_KEY not in results:
836 raise qa_error.Error("RAPI instance-multi-alloc did not deliver "
837 "information about created jobs")
838
839 if len(results[JOBS_KEY]) != len(instance_list):
840 raise qa_error.Error("RAPI instance-multi-alloc failed to return the "
841 "desired number of jobs!")
842
843 for success, job in results[JOBS_KEY]:
844 if success:
845 _WaitForRapiJob(job)
846 else:
847 raise qa_error.Error("Failed to create instance in "
848 "instance-multi-alloc call")
849 except:
850 # Note that although released, it may be that some of the instance creations
851 # have in fact succeeded. Handling this in a better way may be possible, but
852 # is not necessary as the QA has already failed at this point.
853 for instance in instance_list:
854 instance.Release()
855 raise
856
857 return (instance_one, instance_two)
858
859
860 @InstanceCheck(None, INST_DOWN, FIRST_ARG)
861 def TestRapiInstanceRemove(instance, use_client):
862 """Test removing instance via RAPI"""
863 # FIXME: this does not work if LVM is not enabled. Find out if this is a bug
864 # in RAPI or in the test
865 if not qa_config.IsStorageTypeSupported(constants.ST_LVM_VG):
866 return
867
868 if use_client:
869 job_id = _rapi_client.DeleteInstance(instance.name)
870 else:
871 (job_id, ) = _DoTests([
872 ("/2/instances/%s" % instance.name, _VerifyReturnsJob, "DELETE", None),
873 ])
874
875 _WaitForRapiJob(job_id)
876
877
878 @InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
879 def TestRapiInstanceMigrate(instance):
880 """Test migrating instance via RAPI"""
881 if not IsMigrationSupported(instance):
882 print qa_logging.FormatInfo("Instance doesn't support migration, skipping"
883 " test")
884 return
885 # Move to secondary node
886 _WaitForRapiJob(_rapi_client.MigrateInstance(instance.name))
887 qa_utils.RunInstanceCheck(instance, True)
888 # And back to previous primary
889 _WaitForRapiJob(_rapi_client.MigrateInstance(instance.name))
890
891
892 @InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
893 def TestRapiInstanceFailover(instance):
894 """Test failing over instance via RAPI"""
895 if not IsFailoverSupported(instance):
896 print qa_logging.FormatInfo("Instance doesn't support failover, skipping"
897 " test")
898 return
899 # Move to secondary node
900 _WaitForRapiJob(_rapi_client.FailoverInstance(instance.name))
901 qa_utils.RunInstanceCheck(instance, True)
902 # And back to previous primary
903 _WaitForRapiJob(_rapi_client.FailoverInstance(instance.name))
904
905
906 @InstanceCheck(INST_UP, INST_DOWN, FIRST_ARG)
907 def TestRapiInstanceShutdown(instance):
908 """Test stopping an instance via RAPI"""
909 _WaitForRapiJob(_rapi_client.ShutdownInstance(instance.name))
910
911
912 @InstanceCheck(INST_DOWN, INST_UP, FIRST_ARG)
913 def TestRapiInstanceStartup(instance):
914 """Test starting an instance via RAPI"""
915 _WaitForRapiJob(_rapi_client.StartupInstance(instance.name))
916
917
918 @InstanceCheck(INST_DOWN, INST_DOWN, FIRST_ARG)
919 def TestRapiInstanceRenameAndBack(rename_source, rename_target):
920 """Test renaming instance via RAPI
921
922 This must leave the instance with the original name (in the
923 non-failure case).
924
925 """
926 _WaitForRapiJob(_rapi_client.RenameInstance(rename_source, rename_target))
927 qa_utils.RunInstanceCheck(rename_source, False)
928 qa_utils.RunInstanceCheck(rename_target, False)
929 _WaitForRapiJob(_rapi_client.RenameInstance(rename_target, rename_source))
930 qa_utils.RunInstanceCheck(rename_target, False)
931
932
933 @InstanceCheck(INST_DOWN, INST_DOWN, FIRST_ARG)
934 def TestRapiInstanceReinstall(instance):
935 """Test reinstalling an instance via RAPI"""
936 if instance.disk_template == constants.DT_DISKLESS:
937 print qa_logging.FormatInfo("Test not supported for diskless instances")
938 return
939
940 _WaitForRapiJob(_rapi_client.ReinstallInstance(instance.name))
941 # By default, the instance is started again
942 qa_utils.RunInstanceCheck(instance, True)
943
944 # Reinstall again without starting
945 _WaitForRapiJob(_rapi_client.ReinstallInstance(instance.name,
946 no_startup=True))
947
948
949 @InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
950 def TestRapiInstanceReplaceDisks(instance):
951 """Test replacing instance disks via RAPI"""
952 if not IsDiskReplacingSupported(instance):
953 print qa_logging.FormatInfo("Instance doesn't support disk replacing,"
954 " skipping test")
955 return
956 fn = _rapi_client.ReplaceInstanceDisks
957 _WaitForRapiJob(fn(instance.name,
958 mode=constants.REPLACE_DISK_AUTO, disks=[]))
959 _WaitForRapiJob(fn(instance.name,
960 mode=constants.REPLACE_DISK_SEC, disks="0"))
961
962
963 @InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
964 def TestRapiInstanceModify(instance):
965 """Test modifying instance via RAPI"""
966 default_hv = qa_config.GetDefaultHypervisor()
967
968 def _ModifyInstance(**kwargs):
969 _WaitForRapiJob(_rapi_client.ModifyInstance(instance.name, **kwargs))
970
971 _ModifyInstance(beparams={
972 constants.BE_VCPUS: 3,
973 })
974
975 _ModifyInstance(beparams={
976 constants.BE_VCPUS: constants.VALUE_DEFAULT,
977 })
978
979 if default_hv == constants.HT_XEN_PVM:
980 _ModifyInstance(hvparams={
981 constants.HV_KERNEL_ARGS: "single",
982 })
983 _ModifyInstance(hvparams={
984 constants.HV_KERNEL_ARGS: constants.VALUE_DEFAULT,
985 })
986 elif default_hv == constants.HT_XEN_HVM:
987 _ModifyInstance(hvparams={
988 constants.HV_BOOT_ORDER: "acn",
989 })
990 _ModifyInstance(hvparams={
991 constants.HV_BOOT_ORDER: constants.VALUE_DEFAULT,
992 })
993
994
995 @InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
996 def TestRapiInstanceConsole(instance):
997 """Test getting instance console information via RAPI"""
998 result = _rapi_client.GetInstanceConsole(instance.name)
999 console = objects.InstanceConsole.FromDict(result)
1000 AssertEqual(console.Validate(), None)
1001 AssertEqual(console.instance, qa_utils.ResolveInstanceName(instance.name))
1002
1003
1004 @InstanceCheck(INST_DOWN, INST_DOWN, FIRST_ARG)
1005 def TestRapiStoppedInstanceConsole(instance):
1006 """Test getting stopped instance's console information via RAPI"""
1007 try:
1008 _rapi_client.GetInstanceConsole(instance.name)
1009 except rapi.client.GanetiApiError, err:
1010 AssertEqual(err.code, 503)
1011 else:
1012 raise qa_error.Error("Getting console for stopped instance didn't"
1013 " return HTTP 503")
1014
1015
1016 def GetOperatingSystems():
1017 """Retrieves a list of all available operating systems.
1018
1019 """
1020 return _rapi_client.GetOperatingSystems()
1021
1022
1023 def TestInterClusterInstanceMove(src_instance, dest_instance,
1024 inodes, tnode, perform_checks=True):
1025 """Test tools/move-instance"""
1026 master = qa_config.GetMasterNode()
1027
1028 rapi_pw_file = tempfile.NamedTemporaryFile()
1029 rapi_pw_file.write(_rapi_password)
1030 rapi_pw_file.flush()
1031
1032 # Needed only if checks are to be performed
1033 if perform_checks:
1034 dest_instance.SetDiskTemplate(src_instance.disk_template)
1035
1036 # TODO: Run some instance tests before moving back
1037
1038 if len(inodes) > 1:
1039 # No disk template currently requires more than 1 secondary node. If this
1040 # changes, either this test must be skipped or the script must be updated.
1041 assert len(inodes) == 2
1042 snode = inodes[1]
1043 else:
1044 # instance is not redundant, but we still need to pass a node
1045 # (which will be ignored)
1046 snode = tnode
1047 pnode = inodes[0]
1048 # note: pnode:snode are the *current* nodes, so we move it first to
1049 # tnode:pnode, then back to pnode:snode
1050 for current_src_inst, current_dest_inst, target_pnode, target_snode in \
1051 [(src_instance.name, dest_instance.name, tnode.primary, pnode.primary),
1052 (dest_instance.name, src_instance.name, pnode.primary, snode.primary)]:
1053 cmd = [
1054 "../tools/move-instance",
1055 "--verbose",
1056 "--src-ca-file=%s" % _rapi_ca.name,
1057 "--src-username=%s" % _rapi_username,
1058 "--src-password-file=%s" % rapi_pw_file.name,
1059 "--dest-instance-name=%s" % current_dest_inst,
1060 "--dest-primary-node=%s" % target_pnode,
1061 "--dest-secondary-node=%s" % target_snode,
1062 "--net=0:mac=%s" % constants.VALUE_GENERATE,
1063 master.primary,
1064 master.primary,
1065 current_src_inst,
1066 ]
1067
1068 # Some uses of this test might require that RAPI-only commands are used,
1069 # and the checks are command-line based.
1070
1071 if perform_checks:
1072 qa_utils.RunInstanceCheck(current_dest_inst, False)
1073
1074 AssertEqual(StartLocalCommand(cmd).wait(), 0)
1075
1076 if perform_checks:
1077 qa_utils.RunInstanceCheck(current_src_inst, False)
1078 qa_utils.RunInstanceCheck(current_dest_inst, True)
1079
1080
1081 _DRBD_SECRET_RE = re.compile('shared-secret.*"([0-9A-Fa-f]+)"')
1082
1083
1084 def _RetrieveSecret(instance, pnode):
1085 """Retrieves the DRBD secret given an instance object and the primary node.
1086
1087 @type instance: L{qa_config._QaInstance}
1088 @type pnode: L{qa_config._QaNode}
1089
1090 @rtype: string
1091
1092 """
1093 instance_info = GetInstanceInfo(instance.name)
1094
1095 # We are interested in only the first disk on the primary
1096 drbd_minor = instance_info["drbd-minors"][pnode.primary][0]
1097
1098 # This form should work for all DRBD versions
1099 drbd_command = ("drbdsetup show %d; drbdsetup %d show || true" %
1100 (drbd_minor, drbd_minor))
1101 instance_drbd_info = \
1102 qa_utils.GetCommandOutput(pnode.primary, drbd_command)
1103
1104 match_obj = _DRBD_SECRET_RE.search(instance_drbd_info)
1105 if match_obj is None:
1106 raise qa_error.Error("Could not retrieve DRBD secret for instance %s from"
1107 " node %s." % (instance.name, pnode.primary))
1108
1109 return match_obj.groups(0)[0]
1110
1111
1112 def TestInstanceDataCensorship(instance, inodes):
1113 """Test protection of sensitive instance data."""
1114
1115 if instance.disk_template != constants.DT_DRBD8:
1116 print qa_utils.FormatInfo("Only the DRBD secret is a sensitive parameter"
1117 " right now, skipping for non-DRBD instance.")
1118 return
1119
1120 drbd_secret = _RetrieveSecret(instance, inodes[0])
1121
1122 job_id = _rapi_client.GetInstanceInfo(instance.name)
1123 if not _rapi_client.WaitForJobCompletion(job_id):
1124 raise qa_error.Error("Could not fetch instance info for instance %s" %
1125 instance.name)
1126 info_dict = _rapi_client.GetJobStatus(job_id)
1127
1128 if drbd_secret in str(info_dict):
1129 print qa_utils.FormatInfo("DRBD secret: %s" % drbd_secret)
1130 print qa_utils.FormatInfo("Retrieved data\n%s" % str(info_dict))
1131 raise qa_error.Error("Found DRBD secret in contents of RAPI instance info"
1132 " call; see above.")