RAPI: Add support for querying resources
authorMichael Hanselmann <hansmi@google.com>
Fri, 11 Mar 2011 13:25:24 +0000 (14:25 +0100)
committerMichael Hanselmann <hansmi@google.com>
Tue, 15 Mar 2011 13:04:16 +0000 (14:04 +0100)
- Access is only permitted for authenticated clients (queries can return
  sensitive data)
- Filters can be specified when sending a PUT request
- Updates RAPI client, documentation and tests

Signed-off-by: Michael Hanselmann <hansmi@google.com>
Reviewed-by: RenĂ© Nussbaumer <rn@google.com>

doc/rapi.rst
lib/rapi/client.py
lib/rapi/connector.py
lib/rapi/rlib2.py
test/ganeti.rapi.client_unittest.py

index 337f36d..3729973 100644 (file)
@@ -1289,6 +1289,50 @@ to URI like::
 It supports the ``dry-run`` argument.
 
 
+``/2/query/[resource]``
++++++++++++++++++++++++
+
+Requests resource information. Available fields can be found in man
+pages and using ``/2/query/[resource]/fields``. The resource is one of
+:pyeval:`utils.CommaJoin(constants.QR_VIA_RAPI)`. See the :doc:`query2
+design document <design-query2>` for more details.
+
+Supports the following commands: ``GET``, ``PUT``.
+
+``GET``
+~~~~~~~
+
+Returns list of included fields and actual data. Takes a query parameter
+named "fields", containing a comma-separated list of field names. Does
+not support filtering.
+
+``PUT``
+~~~~~~~
+
+Returns list of included fields and actual data. The list of requested
+fields can either be given as the query parameter "fields" or as a body
+parameter with the same name. The optional body parameter "filter" can
+be given and must be either ``null`` or a list containing filter
+operators.
+
+
+``/2/query/[resource]/fields``
+++++++++++++++++++++++++++++++
+
+Request list of available fields for a resource. The resource is one of
+:pyeval:`utils.CommaJoin(constants.QR_VIA_RAPI)`. See the
+:doc:`query2 design document <design-query2>` for more details.
+
+Supports the following commands: ``GET``.
+
+``GET``
+~~~~~~~
+
+Returns a list of field descriptions for available fields. Takes an
+optional query parameter named "fields", containing a comma-separated
+list of field names.
+
+
 ``/2/os``
 +++++++++
 
index 4159b80..b49db8a 100644 (file)
@@ -1583,7 +1583,6 @@ class GanetiRapiClient(object): # pylint: disable-msg=R0904
                              ("/%s/groups/%s/rename" %
                               (GANETI_RAPI_VERSION, group)), None, body)
 
-
   def AssignGroupNodes(self, group, nodes, force=False, dry_run=False):
     """Assigns nodes to a group.
 
@@ -1611,3 +1610,49 @@ class GanetiRapiClient(object): # pylint: disable-msg=R0904
     return self._SendRequest(HTTP_PUT,
                              ("/%s/groups/%s/assign-nodes" %
                              (GANETI_RAPI_VERSION, group)), query, body)
+
+  def Query(self, what, fields, filter_=None):
+    """Retrieves information about resources.
+
+    @type what: string
+    @param what: Resource name, one of L{constants.QR_VIA_RAPI}
+    @type fields: list of string
+    @param fields: Requested fields
+    @type filter_: None or list
+    @param filter_ Query filter
+
+    @rtype: string
+    @return: job id
+
+    """
+    body = {
+      "fields": fields,
+      }
+
+    if filter_ is not None:
+      body["filter"] = filter_
+
+    return self._SendRequest(HTTP_PUT,
+                             ("/%s/query/%s" %
+                              (GANETI_RAPI_VERSION, what)), None, body)
+
+  def QueryFields(self, what, fields=None):
+    """Retrieves available fields for a resource.
+
+    @type what: string
+    @param what: Resource name, one of L{constants.QR_VIA_RAPI}
+    @type fields: list of string
+    @param fields: Requested fields
+
+    @rtype: string
+    @return: job id
+
+    """
+    query = []
+
+    if fields is not None:
+      query.append(("fields", ",".join(fields)))
+
+    return self._SendRequest(HTTP_GET,
+                             ("/%s/query/%s/fields" %
+                              (GANETI_RAPI_VERSION, what)), query, None)
index ed82fca..6243f4a 100644 (file)
@@ -243,6 +243,9 @@ def GetHandlers(node_name_pattern, instance_name_pattern,
     "/2/redistribute-config": rlib2.R_2_redist_config,
     "/2/features": rlib2.R_2_features,
     "/2/modify": rlib2.R_2_cluster_modify,
+    re.compile(r"^/2/query/(%s)$" % query_res_pattern): rlib2.R_2_query,
+    re.compile(r"^/2/query/(%s)/fields$" % query_res_pattern):
+      rlib2.R_2_query_fields,
     }
 
 
index ac4c3fa..f6dcd95 100644 (file)
@@ -1259,6 +1259,81 @@ class R_2_instances_name_console(baserlib.R_Generic):
     return console
 
 
+def _GetQueryFields(args):
+  """
+
+  """
+  try:
+    fields = args["fields"]
+  except KeyError:
+    raise http.HttpBadRequest("Missing 'fields' query argument")
+
+  return _SplitQueryFields(fields[0])
+
+
+def _SplitQueryFields(fields):
+  """
+
+  """
+  return [i.strip() for i in fields.split(",")]
+
+
+class R_2_query(baserlib.R_Generic):
+  """/2/query/[resource] resource.
+
+  """
+  # Results might contain sensitive information
+  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
+
+  def _Query(self, fields, filter_):
+    return baserlib.GetClient().Query(self.items[0], fields, filter_).ToDict()
+
+  def GET(self):
+    """Returns resource information.
+
+    @return: Query result, see L{objects.QueryResponse}
+
+    """
+    return self._Query(_GetQueryFields(self.queryargs), None)
+
+  def PUT(self):
+    """Submits job querying for resources.
+
+    @return: Query result, see L{objects.QueryResponse}
+
+    """
+    body = self.request_body
+
+    baserlib.CheckType(body, dict, "Body contents")
+
+    try:
+      fields = body["fields"]
+    except KeyError:
+      fields = _GetQueryFields(self.queryargs)
+
+    return self._Query(fields, self.request_body.get("filter", None))
+
+
+class R_2_query_fields(baserlib.R_Generic):
+  """/2/query/[resource]/fields resource.
+
+  """
+  def GET(self):
+    """Retrieves list of available fields for a resource.
+
+    @return: List of serialized L{objects.QueryFieldDefinition}
+
+    """
+    try:
+      raw_fields = self.queryargs["fields"]
+    except KeyError:
+      fields = None
+    else:
+      fields = _SplitQueryFields(raw_fields[0])
+
+    return baserlib.GetClient().QueryFields(self.items[0], fields).ToDict()
+
+
 class _R_Tags(baserlib.R_Generic):
   """ Quasiclass for tagging resources
 
index 33ba9a2..2cd1e8d 100755 (executable)
@@ -31,6 +31,8 @@ from ganeti import constants
 from ganeti import http
 from ganeti import serializer
 from ganeti import utils
+from ganeti import query
+from ganeti import objects
 
 from ganeti.rapi import connector
 from ganeti.rapi import rlib2
@@ -1152,6 +1154,55 @@ class GanetiRapiClientTests(testutils.GanetiTestCase):
       self.assertEqual(data["amount"], amount)
       self.assertEqual(self.rapi.CountPending(), 0)
 
+  def testQuery(self):
+    for idx, what in enumerate(constants.QR_VIA_RAPI):
+      for idx2, filter_ in enumerate([None, ["?", "name"]]):
+        job_id = 11010 + (idx << 4) + (idx2 << 16)
+        fields = sorted(query.ALL_FIELDS[what].keys())[:10]
+
+        self.rapi.AddResponse(str(job_id))
+        self.assertEqual(self.client.Query(what, fields, filter_=filter_),
+                         job_id)
+        self.assertItems([what])
+        self.assertHandler(rlib2.R_2_query)
+        self.assertFalse(self.rapi.GetLastHandler().queryargs)
+        data = serializer.LoadJson(self.rapi.GetLastRequestData())
+        self.assertEqual(data["fields"], fields)
+        if filter_ is None:
+          self.assertTrue("filter" not in data)
+        else:
+          self.assertEqual(data["filter"], filter_)
+        self.assertEqual(self.rapi.CountPending(), 0)
+
+  def testQueryFields(self):
+    exp_result = objects.QueryFieldsResponse(fields=[
+      objects.QueryFieldDefinition(name="pnode", title="PNode",
+                                   kind=constants.QFT_NUMBER),
+      objects.QueryFieldDefinition(name="other", title="Other",
+                                   kind=constants.QFT_BOOL),
+      ])
+
+    for what in constants.QR_VIA_RAPI:
+      for fields in [None, ["name", "_unknown_"], ["&", "?|"]]:
+        self.rapi.AddResponse(serializer.DumpJson(exp_result.ToDict()))
+        result = self.client.QueryFields(what, fields=fields)
+        self.assertItems([what])
+        self.assertHandler(rlib2.R_2_query_fields)
+        self.assertFalse(self.rapi.GetLastRequestData())
+
+        queryargs = self.rapi.GetLastHandler().queryargs
+        if fields is None:
+          self.assertFalse(queryargs)
+        else:
+          self.assertEqual(queryargs, {
+            "fields": [",".join(fields)],
+            })
+
+        self.assertEqual(objects.QueryFieldsResponse.FromDict(result).ToDict(),
+                         exp_result.ToDict())
+
+        self.assertEqual(self.rapi.CountPending(), 0)
+
 
 class RapiTestRunner(unittest.TextTestRunner):
   def run(self, *args):