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