From 0fdf247d0395039ed064c724e8d7632296d92628 Mon Sep 17 00:00:00 2001 From: Michael Hanselmann Date: Wed, 18 Apr 2012 18:38:14 +0200 Subject: [PATCH] Convert listing exports to query2 MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit This solves one case where locks are acquired during LUXI queries. Pretty late into the transition I noticed that OpBackupQuery had a “use_locking” parameter for a long time, but didn't use it. Since most of the other changes were already and this allows exports to be listed via RAPI (/2/query) I decided to finish. Signed-off-by: Michael Hanselmann Reviewed-by: Iustin Pop --- NEWS | 7 ++-- lib/client/gnt_backup.py | 44 ++++++++++++++------- lib/cmdlib.py | 85 +++++++++++++++++++++++++++++++---------- lib/constants.py | 9 ++++- lib/query.py | 28 ++++++++++++++ man/gnt-backup.rst | 28 +++++++++++++- qa/ganeti-qa.py | 1 + qa/qa_instance.py | 8 ++++ qa/qa_utils.py | 21 +++++----- test/ganeti.query_unittest.py | 12 ++++-- 10 files changed, 190 insertions(+), 53 deletions(-) diff --git a/NEWS b/NEWS index d2973ff..8b90d0d 100644 --- a/NEWS +++ b/NEWS @@ -15,9 +15,10 @@ Version 2.6.0 beta1 - Removed deprecated ``QueryLocks`` LUXI request. Use ``Query(what=QR_LOCK, ...)`` instead. - The LUXI requests :pyeval:`luxi.REQ_QUERY_JOBS`, - :pyeval:`luxi.REQ_QUERY_INSTANCES`, :pyeval:`luxi.REQ_QUERY_NODES` and - :pyeval:`luxi.REQ_QUERY_GROUPS` are deprecated and will be removed in - a future version. :pyeval:`luxi.REQ_QUERY` should be used instead. + :pyeval:`luxi.REQ_QUERY_INSTANCES`, :pyeval:`luxi.REQ_QUERY_NODES`, + :pyeval:`luxi.REQ_QUERY_GROUPS` and :pyeval:`luxi.REQ_QUERY_EXPORTS` + are deprecated and will be removed in a future version. + :pyeval:`luxi.REQ_QUERY` should be used instead. Version 2.5.0 diff --git a/lib/client/gnt_backup.py b/lib/client/gnt_backup.py index 35190bb..edc8b44 100644 --- a/lib/client/gnt_backup.py +++ b/lib/client/gnt_backup.py @@ -30,6 +30,10 @@ from ganeti.cli import * from ganeti import opcodes from ganeti import constants from ganeti import errors +from ganeti import qlang + + +_LIST_DEF_FIELDS = ["node", "export"] def PrintExportList(opts, args): @@ -42,18 +46,27 @@ def PrintExportList(opts, args): @return: the desired exit code """ - exports = GetClient().QueryExports(opts.nodes, False) - retcode = 0 - for node in exports: - ToStdout("Node: %s", node) - ToStdout("Exports:") - if isinstance(exports[node], list): - for instance_name in exports[node]: - ToStdout("\t%s", instance_name) - else: - ToStdout(" Could not get exports list") - retcode = 1 - return retcode + selected_fields = ParseFields(opts.output, _LIST_DEF_FIELDS) + + qfilter = qlang.MakeSimpleFilter("node", opts.nodes) + + return GenericList(constants.QR_EXPORT, selected_fields, None, opts.units, + opts.separator, not opts.no_headers, + verbose=opts.verbose, qfilter=qfilter) + + +def ListExportFields(opts, args): + """List export fields. + + @param opts: the command line options selected by the user + @type args: list + @param args: fields to list, or empty for all + @rtype: int + @return: the desired exit code + + """ + return GenericListFields(constants.QR_EXPORT, args, opts.separator, + not opts.no_headers) def ExportInstance(opts, args): @@ -122,8 +135,13 @@ import_opts = [ commands = { "list": ( PrintExportList, ARGS_NONE, - [NODE_LIST_OPT], + [NODE_LIST_OPT, NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, VERBOSE_OPT], "", "Lists instance exports available in the ganeti cluster"), + "list-fields": ( + ListExportFields, [ArgUnknown()], + [NOHDR_OPT, SEP_OPT], + "[fields...]", + "Lists all available fields for exports"), "export": ( ExportInstance, ARGS_ONE_INSTANCE, [FORCE_OPT, SINGLE_NODE_OPT, NOSHUTDOWN_OPT, SHUTDOWN_TIMEOUT_OPT, diff --git a/lib/cmdlib.py b/lib/cmdlib.py index f4c6f93..db7685c 100644 --- a/lib/cmdlib.py +++ b/lib/cmdlib.py @@ -493,6 +493,9 @@ class _QueryBase: #: Attribute holding field definitions FIELDS = None + #: Field to sort by + SORT_FIELD = "name" + def __init__(self, qfilter, fields, use_locking): """Initializes this class. @@ -500,7 +503,7 @@ class _QueryBase: self.use_locking = use_locking self.query = query.Query(self.FIELDS, fields, qfilter=qfilter, - namefield="name") + namefield=self.SORT_FIELD) self.requested_data = self.query.RequestedData() self.names = self.query.RequestedNames() @@ -13024,32 +13027,73 @@ class LUBackupQuery(NoHooksLU): """ REQ_BGL = False + def CheckArguments(self): + self.expq = _ExportQuery(qlang.MakeSimpleFilter("node", self.op.nodes), + ["node", "export"], self.op.use_locking) + def ExpandNames(self): - self.needed_locks = {} - self.share_locks[locking.LEVEL_NODE] = 1 - if not self.op.nodes: - self.needed_locks[locking.LEVEL_NODE] = locking.ALL_SET - else: - self.needed_locks[locking.LEVEL_NODE] = \ - _GetWantedNodes(self, self.op.nodes) + self.expq.ExpandNames(self) + + def DeclareLocks(self, level): + self.expq.DeclareLocks(self, level) def Exec(self, feedback_fn): - """Compute the list of all the exported system images. + result = {} - @rtype: dict - @return: a dictionary with the structure node->(export-list) - where export-list is a list of the instances exported on - that node. + for (node, expname) in self.expq.OldStyleQuery(self): + if expname is None: + result[node] = False + else: + result.setdefault(node, []).append(expname) + + return result + + +class _ExportQuery(_QueryBase): + FIELDS = query.EXPORT_FIELDS + + #: The node name is not a unique key for this query + SORT_FIELD = "node" + + def ExpandNames(self, lu): + lu.needed_locks = {} + + # The following variables interact with _QueryBase._GetNames + if self.names: + self.wanted = _GetWantedNodes(lu, self.names) + else: + self.wanted = locking.ALL_SET + + self.do_locking = self.use_locking + + if self.do_locking: + lu.share_locks = _ShareAll() + lu.needed_locks = { + locking.LEVEL_NODE: self.wanted, + } + + def DeclareLocks(self, lu, level): + pass + + def _GetQueryData(self, lu): + """Computes the list of nodes and their attributes. """ - self.nodes = self.owned_locks(locking.LEVEL_NODE) - rpcresult = self.rpc.call_export_list(self.nodes) - result = {} - for node in rpcresult: - if rpcresult[node].fail_msg: - result[node] = False + # Locking is not used + assert not (compat.any(lu.glm.is_owned(level) + for level in locking.LEVELS + if level != locking.LEVEL_CLUSTER) or + self.do_locking or self.use_locking) + + nodes = self._GetNames(lu, lu.cfg.GetNodeList(), locking.LEVEL_NODE) + + result = [] + + for (node, nres) in lu.rpc.call_export_list(nodes).items(): + if nres.fail_msg: + result.append((node, None)) else: - result[node] = rpcresult[node].payload + result.extend((node, expname) for expname in nres.payload) return result @@ -15174,6 +15218,7 @@ _QUERY_IMPL = { constants.QR_NODE: _NodeQuery, constants.QR_GROUP: _GroupQuery, constants.QR_OS: _OsQuery, + constants.QR_EXPORT: _ExportQuery, } assert set(_QUERY_IMPL.keys()) == constants.QR_VIA_OP diff --git a/lib/constants.py b/lib/constants.py index 040d186..68dc96c 100644 --- a/lib/constants.py +++ b/lib/constants.py @@ -1631,9 +1631,16 @@ QR_LOCK = "lock" QR_GROUP = "group" QR_OS = "os" QR_JOB = "job" +QR_EXPORT = "export" #: List of resources which can be queried using L{opcodes.OpQuery} -QR_VIA_OP = frozenset([QR_INSTANCE, QR_NODE, QR_GROUP, QR_OS]) +QR_VIA_OP = frozenset([ + QR_INSTANCE, + QR_NODE, + QR_GROUP, + QR_OS, + QR_EXPORT, + ]) #: List of resources which can be queried using Local UniX Interface QR_VIA_LUXI = QR_VIA_OP.union([ diff --git a/lib/query.py b/lib/query.py index b2880a4..2a1213b 100644 --- a/lib/query.py +++ b/lib/query.py @@ -2240,6 +2240,30 @@ def _BuildJobFields(): return _PrepareFieldList(fields, []) +def _GetExportName(_, (node_name, expname)): # pylint: disable=W0613 + """Returns an export name if available. + + """ + if expname is None: + return _FS_UNAVAIL + else: + return expname + + +def _BuildExportFields(): + """Builds list of fields for exports. + + """ + fields = [ + (_MakeField("node", "Node", QFT_TEXT, "Node name"), + None, QFF_HOSTNAME, lambda _, (node_name, expname): node_name), + (_MakeField("export", "Export", QFT_TEXT, "Export name"), + None, 0, _GetExportName), + ] + + return _PrepareFieldList(fields, []) + + #: Fields available for node queries NODE_FIELDS = _BuildNodeFields() @@ -2258,6 +2282,9 @@ OS_FIELDS = _BuildOsFields() #: Fields available for job queries JOB_FIELDS = _BuildJobFields() +#: Fields available for exports +EXPORT_FIELDS = _BuildExportFields() + #: All available resources ALL_FIELDS = { constants.QR_INSTANCE: INSTANCE_FIELDS, @@ -2266,6 +2293,7 @@ ALL_FIELDS = { constants.QR_GROUP: GROUP_FIELDS, constants.QR_OS: OS_FIELDS, constants.QR_JOB: JOB_FIELDS, + constants.QR_EXPORT: EXPORT_FIELDS, } #: All available field lists diff --git a/man/gnt-backup.rst b/man/gnt-backup.rst index 4dd66df..da56299 100644 --- a/man/gnt-backup.rst +++ b/man/gnt-backup.rst @@ -228,16 +228,40 @@ Explicit configuration example:: LIST ~~~~ -**list** [\--node=*NODE*] +| **list** [\--node=*NODE*] [\--no-headers] [\--separator=*SEPARATOR*] +| [-o *[+]FIELD,...*] Lists the exports currently available in the default directory in all the nodes of the current cluster, or optionally only a subset of them specified using the ``--node`` option (which can be used multiple times) +The ``--no-headers`` option will skip the initial header line. The +``--separator`` option takes an argument which denotes what will be +used between the output fields. Both these options are to help +scripting. + +The ``-o`` option takes a comma-separated list of output fields. +The available fields and their meaning are: + +@QUERY_FIELDS_EXPORT@ + +If the value of the option starts with the character ``+``, the new +fields will be added to the default list. This allows one to quickly +see the default list plus a few other fields, instead of retyping +the entire list of fields. + Example:: - # gnt-backup list --nodes node1 --nodes node2 + # gnt-backup list --node node1 --node node2 + + +LIST-FIELDS +~~~~~~~~~~~ + +**list-fields** [field...] + +Lists available fields for exports. REMOVE diff --git a/qa/ganeti-qa.py b/qa/ganeti-qa.py index 2926202..91cd506 100755 --- a/qa/ganeti-qa.py +++ b/qa/ganeti-qa.py @@ -149,6 +149,7 @@ def SetupCluster(rapi_user, rapi_secret): RunTestIf("node-list", qa_node.TestNodeListFields) RunTestIf("instance-list", qa_instance.TestInstanceListFields) RunTestIf("job-list", qa_job.TestJobListFields) + RunTestIf("instance-export", qa_instance.TestBackupListFields) RunTestIf("node-info", qa_node.TestNodeInfo) diff --git a/qa/qa_instance.py b/qa/qa_instance.py index dce7936..9834c75 100644 --- a/qa/qa_instance.py +++ b/qa/qa_instance.py @@ -358,6 +358,14 @@ def TestBackupList(expnode): """gnt-backup list""" AssertCommand(["gnt-backup", "list", "--node=%s" % expnode["primary"]]) + qa_utils.GenericQueryTest("gnt-backup", query.EXPORT_FIELDS.keys(), + namefield=None, test_unknown=False) + + +def TestBackupListFields(): + """gnt-backup list-fields""" + qa_utils.GenericQueryFieldsTest("gnt-backup", query.EXPORT_FIELDS.keys()) + def _TestInstanceDiskFailure(instance, node, node2, onmaster): """Testing disk failure.""" diff --git a/qa/qa_utils.py b/qa/qa_utils.py index 7bdd64f..f0ef63c 100644 --- a/qa/qa_utils.py +++ b/qa/qa_utils.py @@ -426,19 +426,20 @@ def GenericQueryTest(cmd, fields, namefield="name", test_unknown=True): for testfields in _SelectQueryFields(rnd, fields): AssertCommand([cmd, "list", "--output", ",".join(testfields)]) - namelist_fn = compat.partial(_List, cmd, [namefield]) + if namefield is not None: + namelist_fn = compat.partial(_List, cmd, [namefield]) - # When no names were requested, the list must be sorted - names = namelist_fn(None) - AssertEqual(names, utils.NiceSort(names)) + # When no names were requested, the list must be sorted + names = namelist_fn(None) + AssertEqual(names, utils.NiceSort(names)) - # When requesting specific names, the order must be kept - revnames = list(reversed(names)) - AssertEqual(namelist_fn(revnames), revnames) + # When requesting specific names, the order must be kept + revnames = list(reversed(names)) + AssertEqual(namelist_fn(revnames), revnames) - randnames = list(names) - rnd.shuffle(randnames) - AssertEqual(namelist_fn(randnames), randnames) + randnames = list(names) + rnd.shuffle(randnames) + AssertEqual(namelist_fn(randnames), randnames) if test_unknown: # Listing unknown items must fail diff --git a/test/ganeti.query_unittest.py b/test/ganeti.query_unittest.py index 9f93dc6..5e00d24 100755 --- a/test/ganeti.query_unittest.py +++ b/test/ganeti.query_unittest.py @@ -1128,9 +1128,11 @@ class TestQueryFields(unittest.TestCase): class TestQueryFilter(unittest.TestCase): def testRequestedNames(self): - for fielddefs in query.ALL_FIELD_LISTS: - if "id" in fielddefs: + for (what, fielddefs) in query.ALL_FIELDS.items(): + if what == constants.QR_JOB: namefield = "id" + elif what == constants.QR_EXPORT: + namefield = "export" else: namefield = "name" @@ -1207,9 +1209,11 @@ class TestQueryFilter(unittest.TestCase): def testCompileFilter(self): levels_max = query._FilterCompilerHelper._LEVELS_MAX - for fielddefs in query.ALL_FIELD_LISTS: - if "id" in fielddefs: + for (what, fielddefs) in query.ALL_FIELDS.items(): + if what == constants.QR_JOB: namefield = "id" + elif what == constants.QR_EXPORT: + namefield = "export" else: namefield = "name" -- 1.7.10.4