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