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