Commit 0fdf247d authored by Michael Hanselmann's avatar Michael Hanselmann
Browse files

Convert listing exports to query2



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: default avatarMichael Hanselmann <hansmi@google.com>
Reviewed-by: default avatarIustin Pop <iustin@google.com>
parent dd27bc21
......@@ -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
......
......@@ -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,
......
......@@ -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
......
......@@ -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([
......
......@@ -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
......
......@@ -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
......
......@@ -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)
......
......@@ -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."""
......
......@@ -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
......
......@@ -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"
......
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