diff --git a/doc/rapi.rst b/doc/rapi.rst index 337f36dc18db091ee4564140771a7d267b8faf7f..37299733e149692b5dd26589af956b0e287ba25d 100644 --- a/doc/rapi.rst +++ b/doc/rapi.rst @@ -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`` +++++++++ diff --git a/lib/rapi/client.py b/lib/rapi/client.py index 4159b80fc859a06d3f009cf37756d61204ce8406..b49db8a324339fdb49bd3684d86b9202fd22bf49 100644 --- a/lib/rapi/client.py +++ b/lib/rapi/client.py @@ -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) diff --git a/lib/rapi/connector.py b/lib/rapi/connector.py index ed82fca67c05c23cd84ce556ca16453f85b0d2cc..6243f4a1bf2ff12a9f1d70ebc521897ce0c7bc50 100644 --- a/lib/rapi/connector.py +++ b/lib/rapi/connector.py @@ -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, } diff --git a/lib/rapi/rlib2.py b/lib/rapi/rlib2.py index ac4c3fa5c8980b5cb67d7f32fd559a05b39cba32..f6dcd951da5cb8151b77fe9b6f15a9bbbdb90109 100644 --- a/lib/rapi/rlib2.py +++ b/lib/rapi/rlib2.py @@ -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 diff --git a/test/ganeti.rapi.client_unittest.py b/test/ganeti.rapi.client_unittest.py index 33ba9a2327ab633009963700b5eec2db22459119..2cd1e8d0d6dc041dac5b5616e0bcd4401d6ca3ac 100755 --- a/test/ganeti.rapi.client_unittest.py +++ b/test/ganeti.rapi.client_unittest.py @@ -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):