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