Commit 208a6cff authored by Michael Hanselmann's avatar Michael Hanselmann

RAPI: Add support for querying resources

- 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: default avatarMichael Hanselmann <hansmi@google.com>
Reviewed-by: default avatarRené Nussbaumer <rn@google.com>
parent 1c7fd467
......@@ -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``
+++++++++
......
......@@ -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)
......@@ -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,
}
......
......@@ -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
......
......@@ -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):
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment