Stricter check for OS modifications passed to OpClusterSetParams
[ganeti-local] / qa / qa_rapi.py
1 #
2 #
3
4 # Copyright (C) 2007, 2008, 2009, 2010, 2011, 2012 Google Inc.
5 #
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful, but
12 # WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14 # General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19 # 02110-1301, USA.
20
21
22 """Remote API QA tests.
23
24 """
25
26 import tempfile
27 import random
28
29 from ganeti import utils
30 from ganeti import constants
31 from ganeti import errors
32 from ganeti import cli
33 from ganeti import rapi
34 from ganeti import objects
35 from ganeti import query
36 from ganeti import compat
37 from ganeti import qlang
38
39 import ganeti.rapi.client        # pylint: disable=W0611
40 import ganeti.rapi.client_utils
41
42 import qa_config
43 import qa_utils
44 import qa_error
45
46 from qa_utils import (AssertEqual, AssertIn, AssertMatch, StartLocalCommand)
47
48
49 _rapi_ca = None
50 _rapi_client = None
51 _rapi_username = None
52 _rapi_password = None
53
54
55 def Setup(username, password):
56   """Configures the RAPI client.
57
58   """
59   # pylint: disable=W0603
60   # due to global usage
61   global _rapi_ca
62   global _rapi_client
63   global _rapi_username
64   global _rapi_password
65
66   _rapi_username = username
67   _rapi_password = password
68
69   master = qa_config.GetMasterNode()
70
71   # Load RAPI certificate from master node
72   cmd = ["cat", constants.RAPI_CERT_FILE]
73
74   # Write to temporary file
75   _rapi_ca = tempfile.NamedTemporaryFile()
76   _rapi_ca.write(qa_utils.GetCommandOutput(master["primary"],
77                                            utils.ShellQuoteArgs(cmd)))
78   _rapi_ca.flush()
79
80   port = qa_config.get("rapi-port", default=constants.DEFAULT_RAPI_PORT)
81   cfg_curl = rapi.client.GenericCurlConfig(cafile=_rapi_ca.name,
82                                            proxy="")
83
84   _rapi_client = rapi.client.GanetiRapiClient(master["primary"], port=port,
85                                               username=username,
86                                               password=password,
87                                               curl_config_fn=cfg_curl)
88
89   print "RAPI protocol version: %s" % _rapi_client.GetVersion()
90
91
92 INSTANCE_FIELDS = ("name", "os", "pnode", "snodes",
93                    "admin_state",
94                    "disk_template", "disk.sizes",
95                    "nic.ips", "nic.macs", "nic.modes", "nic.links",
96                    "beparams", "hvparams",
97                    "oper_state", "oper_ram", "oper_vcpus", "status", "tags")
98
99 NODE_FIELDS = ("name", "dtotal", "dfree",
100                "mtotal", "mnode", "mfree",
101                "pinst_cnt", "sinst_cnt", "tags")
102
103 GROUP_FIELDS = frozenset([
104   "name", "uuid",
105   "alloc_policy",
106   "node_cnt", "node_list",
107   ])
108
109 JOB_FIELDS = frozenset([
110   "id", "ops", "status", "summary",
111   "opstatus", "opresult", "oplog",
112   "received_ts", "start_ts", "end_ts",
113   ])
114
115 LIST_FIELDS = ("id", "uri")
116
117
118 def Enabled():
119   """Return whether remote API tests should be run.
120
121   """
122   return qa_config.TestEnabled("rapi")
123
124
125 def _DoTests(uris):
126   # pylint: disable=W0212
127   # due to _SendRequest usage
128   results = []
129
130   for uri, verify, method, body in uris:
131     assert uri.startswith("/")
132
133     print "%s %s" % (method, uri)
134     data = _rapi_client._SendRequest(method, uri, None, body)
135
136     if verify is not None:
137       if callable(verify):
138         verify(data)
139       else:
140         AssertEqual(data, verify)
141
142     results.append(data)
143
144   return results
145
146
147 def _VerifyReturnsJob(data):
148   AssertMatch(data, r"^\d+$")
149
150
151 def TestVersion():
152   """Testing remote API version.
153
154   """
155   _DoTests([
156     ("/version", constants.RAPI_VERSION, "GET", None),
157     ])
158
159
160 def TestEmptyCluster():
161   """Testing remote API on an empty cluster.
162
163   """
164   master = qa_config.GetMasterNode()
165   master_full = qa_utils.ResolveNodeName(master)
166
167   def _VerifyInfo(data):
168     AssertIn("name", data)
169     AssertIn("master", data)
170     AssertEqual(data["master"], master_full)
171
172   def _VerifyNodes(data):
173     master_entry = {
174       "id": master_full,
175       "uri": "/2/nodes/%s" % master_full,
176       }
177     AssertIn(master_entry, data)
178
179   def _VerifyNodesBulk(data):
180     for node in data:
181       for entry in NODE_FIELDS:
182         AssertIn(entry, node)
183
184   def _VerifyGroups(data):
185     default_group = {
186       "name": constants.INITIAL_NODE_GROUP_NAME,
187       "uri": "/2/groups/" + constants.INITIAL_NODE_GROUP_NAME,
188       }
189     AssertIn(default_group, data)
190
191   def _VerifyGroupsBulk(data):
192     for group in data:
193       for field in GROUP_FIELDS:
194         AssertIn(field, group)
195
196   _DoTests([
197     ("/", None, "GET", None),
198     ("/2/info", _VerifyInfo, "GET", None),
199     ("/2/tags", None, "GET", None),
200     ("/2/nodes", _VerifyNodes, "GET", None),
201     ("/2/nodes?bulk=1", _VerifyNodesBulk, "GET", None),
202     ("/2/groups", _VerifyGroups, "GET", None),
203     ("/2/groups?bulk=1", _VerifyGroupsBulk, "GET", None),
204     ("/2/instances", [], "GET", None),
205     ("/2/instances?bulk=1", [], "GET", None),
206     ("/2/os", None, "GET", None),
207     ])
208
209   # Test HTTP Not Found
210   for method in ["GET", "PUT", "POST", "DELETE"]:
211     try:
212       _DoTests([("/99/resource/not/here/99", None, method, None)])
213     except rapi.client.GanetiApiError, err:
214       AssertEqual(err.code, 404)
215     else:
216       raise qa_error.Error("Non-existent resource didn't return HTTP 404")
217
218   # Test HTTP Not Implemented
219   for method in ["PUT", "POST", "DELETE"]:
220     try:
221       _DoTests([("/version", None, method, None)])
222     except rapi.client.GanetiApiError, err:
223       AssertEqual(err.code, 501)
224     else:
225       raise qa_error.Error("Non-implemented method didn't fail")
226
227
228 def TestRapiQuery():
229   """Testing resource queries via remote API.
230
231   """
232   master_name = qa_utils.ResolveNodeName(qa_config.GetMasterNode())
233   rnd = random.Random(7818)
234
235   for what in constants.QR_VIA_RAPI:
236     all_fields = query.ALL_FIELDS[what].keys()
237     rnd.shuffle(all_fields)
238
239     # No fields, should return everything
240     result = _rapi_client.QueryFields(what)
241     qresult = objects.QueryFieldsResponse.FromDict(result)
242     AssertEqual(len(qresult.fields), len(all_fields))
243
244     # One field
245     result = _rapi_client.QueryFields(what, fields=["name"])
246     qresult = objects.QueryFieldsResponse.FromDict(result)
247     AssertEqual(len(qresult.fields), 1)
248
249     # Specify all fields, order must be correct
250     result = _rapi_client.QueryFields(what, fields=all_fields)
251     qresult = objects.QueryFieldsResponse.FromDict(result)
252     AssertEqual(len(qresult.fields), len(all_fields))
253     AssertEqual([fdef.name for fdef in qresult.fields], all_fields)
254
255     # Unknown field
256     result = _rapi_client.QueryFields(what, fields=["_unknown!"])
257     qresult = objects.QueryFieldsResponse.FromDict(result)
258     AssertEqual(len(qresult.fields), 1)
259     AssertEqual(qresult.fields[0].name, "_unknown!")
260     AssertEqual(qresult.fields[0].kind, constants.QFT_UNKNOWN)
261
262     # Try once more, this time without the client
263     _DoTests([
264       ("/2/query/%s/fields" % what, None, "GET", None),
265       ("/2/query/%s/fields?fields=name,name,%s" % (what, all_fields[0]),
266        None, "GET", None),
267       ])
268
269     # Try missing query argument
270     try:
271       _DoTests([
272         ("/2/query/%s" % what, None, "GET", None),
273         ])
274     except rapi.client.GanetiApiError, err:
275       AssertEqual(err.code, 400)
276     else:
277       raise qa_error.Error("Request missing 'fields' parameter didn't fail")
278
279     def _Check(exp_fields, data):
280       qresult = objects.QueryResponse.FromDict(data)
281       AssertEqual([fdef.name for fdef in qresult.fields], exp_fields)
282       if not isinstance(qresult.data, list):
283         raise qa_error.Error("Query did not return a list")
284
285     _DoTests([
286       # Specify fields in query
287       ("/2/query/%s?fields=%s" % (what, ",".join(all_fields)),
288        compat.partial(_Check, all_fields), "GET", None),
289
290       ("/2/query/%s?fields=name" % what,
291        compat.partial(_Check, ["name"]), "GET", None),
292
293       # Note the spaces
294       ("/2/query/%s?fields=name,%%20name%%09,name%%20" % what,
295        compat.partial(_Check, ["name"] * 3), "GET", None),
296
297       # PUT with fields in query
298       ("/2/query/%s?fields=name" % what,
299        compat.partial(_Check, ["name"]), "PUT", {}),
300
301       # Fields in body
302       ("/2/query/%s" % what, compat.partial(_Check, all_fields), "PUT", {
303          "fields": all_fields,
304          }),
305
306       ("/2/query/%s" % what, compat.partial(_Check, ["name"] * 4), "PUT", {
307          "fields": ["name"] * 4,
308          }),
309       ])
310
311     def _CheckFilter():
312       _DoTests([
313         # With filter
314         ("/2/query/%s" % what, compat.partial(_Check, all_fields), "PUT", {
315            "fields": all_fields,
316            "filter": [qlang.OP_TRUE, "name"],
317            }),
318         ])
319
320     if what == constants.QR_LOCK:
321       # Locks can't be filtered
322       try:
323         _CheckFilter()
324       except rapi.client.GanetiApiError, err:
325         AssertEqual(err.code, 500)
326       else:
327         raise qa_error.Error("Filtering locks didn't fail")
328     else:
329       _CheckFilter()
330
331     if what == constants.QR_NODE:
332       # Test with filter
333       (nodes, ) = _DoTests([("/2/query/%s" % what,
334         compat.partial(_Check, ["name", "master"]), "PUT", {
335         "fields": ["name", "master"],
336         "filter": [qlang.OP_TRUE, "master"],
337         })])
338       qresult = objects.QueryResponse.FromDict(nodes)
339       AssertEqual(qresult.data, [
340         [[constants.RS_NORMAL, master_name], [constants.RS_NORMAL, True]],
341         ])
342
343
344 def TestInstance(instance):
345   """Testing getting instance(s) info via remote API.
346
347   """
348   def _VerifyInstance(data):
349     for entry in INSTANCE_FIELDS:
350       AssertIn(entry, data)
351
352   def _VerifyInstancesList(data):
353     for instance in data:
354       for entry in LIST_FIELDS:
355         AssertIn(entry, instance)
356
357   def _VerifyInstancesBulk(data):
358     for instance_data in data:
359       _VerifyInstance(instance_data)
360
361   _DoTests([
362     ("/2/instances/%s" % instance["name"], _VerifyInstance, "GET", None),
363     ("/2/instances", _VerifyInstancesList, "GET", None),
364     ("/2/instances?bulk=1", _VerifyInstancesBulk, "GET", None),
365     ("/2/instances/%s/activate-disks" % instance["name"],
366      _VerifyReturnsJob, "PUT", None),
367     ("/2/instances/%s/deactivate-disks" % instance["name"],
368      _VerifyReturnsJob, "PUT", None),
369     ])
370
371   # Test OpBackupPrepare
372   (job_id, ) = _DoTests([
373     ("/2/instances/%s/prepare-export?mode=%s" %
374      (instance["name"], constants.EXPORT_MODE_REMOTE),
375      _VerifyReturnsJob, "PUT", None),
376     ])
377
378   result = _WaitForRapiJob(job_id)[0]
379   AssertEqual(len(result["handshake"]), 3)
380   AssertEqual(result["handshake"][0], constants.RIE_VERSION)
381   AssertEqual(len(result["x509_key_name"]), 3)
382   AssertIn("-----BEGIN CERTIFICATE-----", result["x509_ca"])
383
384
385 def TestNode(node):
386   """Testing getting node(s) info via remote API.
387
388   """
389   def _VerifyNode(data):
390     for entry in NODE_FIELDS:
391       AssertIn(entry, data)
392
393   def _VerifyNodesList(data):
394     for node in data:
395       for entry in LIST_FIELDS:
396         AssertIn(entry, node)
397
398   def _VerifyNodesBulk(data):
399     for node_data in data:
400       _VerifyNode(node_data)
401
402   _DoTests([
403     ("/2/nodes/%s" % node["primary"], _VerifyNode, "GET", None),
404     ("/2/nodes", _VerifyNodesList, "GET", None),
405     ("/2/nodes?bulk=1", _VerifyNodesBulk, "GET", None),
406     ])
407
408
409 def TestTags(kind, name, tags):
410   """Tests .../tags resources.
411
412   """
413   if kind == constants.TAG_CLUSTER:
414     uri = "/2/tags"
415   elif kind == constants.TAG_NODE:
416     uri = "/2/nodes/%s/tags" % name
417   elif kind == constants.TAG_INSTANCE:
418     uri = "/2/instances/%s/tags" % name
419   elif kind == constants.TAG_NODEGROUP:
420     uri = "/2/groups/%s/tags" % name
421   else:
422     raise errors.ProgrammerError("Unknown tag kind")
423
424   def _VerifyTags(data):
425     AssertEqual(sorted(tags), sorted(data))
426
427   queryargs = "&".join("tag=%s" % i for i in tags)
428
429   # Add tags
430   (job_id, ) = _DoTests([
431     ("%s?%s" % (uri, queryargs), _VerifyReturnsJob, "PUT", None),
432     ])
433   _WaitForRapiJob(job_id)
434
435   # Retrieve tags
436   _DoTests([
437     (uri, _VerifyTags, "GET", None),
438     ])
439
440   # Remove tags
441   (job_id, ) = _DoTests([
442     ("%s?%s" % (uri, queryargs), _VerifyReturnsJob, "DELETE", None),
443     ])
444   _WaitForRapiJob(job_id)
445
446
447 def _WaitForRapiJob(job_id):
448   """Waits for a job to finish.
449
450   """
451   def _VerifyJob(data):
452     AssertEqual(data["id"], job_id)
453     for field in JOB_FIELDS:
454       AssertIn(field, data)
455
456   _DoTests([
457     ("/2/jobs/%s" % job_id, _VerifyJob, "GET", None),
458     ])
459
460   return rapi.client_utils.PollJob(_rapi_client, job_id,
461                                    cli.StdioJobPollReportCb())
462
463
464 def TestRapiNodeGroups():
465   """Test several node group operations using RAPI.
466
467   """
468   groups = qa_config.get("groups", {})
469   group1, group2, group3 = groups.get("inexistent-groups",
470                                       ["group1", "group2", "group3"])[:3]
471
472   # Create a group with no attributes
473   body = {
474     "name": group1,
475     }
476
477   (job_id, ) = _DoTests([
478     ("/2/groups", _VerifyReturnsJob, "POST", body),
479     ])
480
481   _WaitForRapiJob(job_id)
482
483   # Create a group specifying alloc_policy
484   body = {
485     "name": group2,
486     "alloc_policy": constants.ALLOC_POLICY_UNALLOCABLE,
487     }
488
489   (job_id, ) = _DoTests([
490     ("/2/groups", _VerifyReturnsJob, "POST", body),
491     ])
492
493   _WaitForRapiJob(job_id)
494
495   # Modify alloc_policy
496   body = {
497     "alloc_policy": constants.ALLOC_POLICY_UNALLOCABLE,
498     }
499
500   (job_id, ) = _DoTests([
501     ("/2/groups/%s/modify" % group1, _VerifyReturnsJob, "PUT", body),
502     ])
503
504   _WaitForRapiJob(job_id)
505
506   # Rename a group
507   body = {
508     "new_name": group3,
509     }
510
511   (job_id, ) = _DoTests([
512     ("/2/groups/%s/rename" % group2, _VerifyReturnsJob, "PUT", body),
513     ])
514
515   _WaitForRapiJob(job_id)
516
517   # Delete groups
518   for group in [group1, group3]:
519     (job_id, ) = _DoTests([
520       ("/2/groups/%s" % group, _VerifyReturnsJob, "DELETE", None),
521       ])
522
523     _WaitForRapiJob(job_id)
524
525
526 def TestRapiInstanceAdd(node, use_client):
527   """Test adding a new instance via RAPI"""
528   instance = qa_config.AcquireInstance()
529   try:
530     disk_sizes = [utils.ParseUnit(size) for size in qa_config.get("disk")]
531     disks = [{"size": size} for size in disk_sizes]
532     nics = [{}]
533
534     beparams = {
535       constants.BE_MAXMEM: utils.ParseUnit(qa_config.get(constants.BE_MAXMEM)),
536       constants.BE_MINMEM: utils.ParseUnit(qa_config.get(constants.BE_MINMEM)),
537       }
538
539     if use_client:
540       job_id = _rapi_client.CreateInstance(constants.INSTANCE_CREATE,
541                                            instance["name"],
542                                            constants.DT_PLAIN,
543                                            disks, nics,
544                                            os=qa_config.get("os"),
545                                            pnode=node["primary"],
546                                            beparams=beparams)
547     else:
548       body = {
549         "__version__": 1,
550         "mode": constants.INSTANCE_CREATE,
551         "name": instance["name"],
552         "os_type": qa_config.get("os"),
553         "disk_template": constants.DT_PLAIN,
554         "pnode": node["primary"],
555         "beparams": beparams,
556         "disks": disks,
557         "nics": nics,
558         }
559
560       (job_id, ) = _DoTests([
561         ("/2/instances", _VerifyReturnsJob, "POST", body),
562         ])
563
564     _WaitForRapiJob(job_id)
565
566     return instance
567   except:
568     qa_config.ReleaseInstance(instance)
569     raise
570
571
572 def TestRapiInstanceRemove(instance, use_client):
573   """Test removing instance via RAPI"""
574   if use_client:
575     job_id = _rapi_client.DeleteInstance(instance["name"])
576   else:
577     (job_id, ) = _DoTests([
578       ("/2/instances/%s" % instance["name"], _VerifyReturnsJob, "DELETE", None),
579       ])
580
581   _WaitForRapiJob(job_id)
582
583   qa_config.ReleaseInstance(instance)
584
585
586 def TestRapiInstanceMigrate(instance):
587   """Test migrating instance via RAPI"""
588   # Move to secondary node
589   _WaitForRapiJob(_rapi_client.MigrateInstance(instance["name"]))
590   # And back to previous primary
591   _WaitForRapiJob(_rapi_client.MigrateInstance(instance["name"]))
592
593
594 def TestRapiInstanceFailover(instance):
595   """Test failing over instance via RAPI"""
596   # Move to secondary node
597   _WaitForRapiJob(_rapi_client.FailoverInstance(instance["name"]))
598   # And back to previous primary
599   _WaitForRapiJob(_rapi_client.FailoverInstance(instance["name"]))
600
601
602 def TestRapiInstanceShutdown(instance):
603   """Test stopping an instance via RAPI"""
604   _WaitForRapiJob(_rapi_client.ShutdownInstance(instance["name"]))
605
606
607 def TestRapiInstanceStartup(instance):
608   """Test starting an instance via RAPI"""
609   _WaitForRapiJob(_rapi_client.StartupInstance(instance["name"]))
610
611
612 def TestRapiInstanceRenameAndBack(rename_source, rename_target):
613   """Test renaming instance via RAPI
614
615   This must leave the instance with the original name (in the
616   non-failure case).
617
618   """
619   _WaitForRapiJob(_rapi_client.RenameInstance(rename_source, rename_target))
620   _WaitForRapiJob(_rapi_client.RenameInstance(rename_target, rename_source))
621
622
623 def TestRapiInstanceReinstall(instance):
624   """Test reinstalling an instance via RAPI"""
625   _WaitForRapiJob(_rapi_client.ReinstallInstance(instance["name"]))
626
627
628 def TestRapiInstanceReplaceDisks(instance):
629   """Test replacing instance disks via RAPI"""
630   _WaitForRapiJob(_rapi_client.ReplaceInstanceDisks(instance["name"],
631     mode=constants.REPLACE_DISK_AUTO, disks=[]))
632   _WaitForRapiJob(_rapi_client.ReplaceInstanceDisks(instance["name"],
633     mode=constants.REPLACE_DISK_SEC, disks="0"))
634
635
636 def TestRapiInstanceModify(instance):
637   """Test modifying instance via RAPI"""
638   def _ModifyInstance(**kwargs):
639     _WaitForRapiJob(_rapi_client.ModifyInstance(instance["name"], **kwargs))
640
641   _ModifyInstance(hvparams={
642     constants.HV_KERNEL_ARGS: "single",
643     })
644
645   _ModifyInstance(beparams={
646     constants.BE_VCPUS: 3,
647     })
648
649   _ModifyInstance(beparams={
650     constants.BE_VCPUS: constants.VALUE_DEFAULT,
651     })
652
653   _ModifyInstance(hvparams={
654     constants.HV_KERNEL_ARGS: constants.VALUE_DEFAULT,
655     })
656
657
658 def TestRapiInstanceConsole(instance):
659   """Test getting instance console information via RAPI"""
660   result = _rapi_client.GetInstanceConsole(instance["name"])
661   console = objects.InstanceConsole.FromDict(result)
662   AssertEqual(console.Validate(), True)
663   AssertEqual(console.instance, qa_utils.ResolveInstanceName(instance["name"]))
664
665
666 def TestRapiStoppedInstanceConsole(instance):
667   """Test getting stopped instance's console information via RAPI"""
668   try:
669     _rapi_client.GetInstanceConsole(instance["name"])
670   except rapi.client.GanetiApiError, err:
671     AssertEqual(err.code, 503)
672   else:
673     raise qa_error.Error("Getting console for stopped instance didn't"
674                          " return HTTP 503")
675
676
677 def GetOperatingSystems():
678   """Retrieves a list of all available operating systems.
679
680   """
681   return _rapi_client.GetOperatingSystems()
682
683
684 def TestInterClusterInstanceMove(src_instance, dest_instance,
685                                  pnode, snode, tnode):
686   """Test tools/move-instance"""
687   master = qa_config.GetMasterNode()
688
689   rapi_pw_file = tempfile.NamedTemporaryFile()
690   rapi_pw_file.write(_rapi_password)
691   rapi_pw_file.flush()
692
693   # TODO: Run some instance tests before moving back
694
695   if snode is None:
696     # instance is not redundant, but we still need to pass a node
697     # (which will be ignored)
698     fsec = tnode
699   else:
700     fsec = snode
701   # note: pnode:snode are the *current* nodes, so we move it first to
702   # tnode:pnode, then back to pnode:snode
703   for si, di, pn, sn in [(src_instance["name"], dest_instance["name"],
704                           tnode["primary"], pnode["primary"]),
705                          (dest_instance["name"], src_instance["name"],
706                           pnode["primary"], fsec["primary"])]:
707     cmd = [
708       "../tools/move-instance",
709       "--verbose",
710       "--src-ca-file=%s" % _rapi_ca.name,
711       "--src-username=%s" % _rapi_username,
712       "--src-password-file=%s" % rapi_pw_file.name,
713       "--dest-instance-name=%s" % di,
714       "--dest-primary-node=%s" % pn,
715       "--dest-secondary-node=%s" % sn,
716       "--net=0:mac=%s" % constants.VALUE_GENERATE,
717       master["primary"],
718       master["primary"],
719       si,
720       ]
721
722     AssertEqual(StartLocalCommand(cmd).wait(), 0)