Convert listing exports to query2
authorMichael Hanselmann <hansmi@google.com>
Wed, 18 Apr 2012 16:38:14 +0000 (18:38 +0200)
committerMichael Hanselmann <hansmi@google.com>
Thu, 19 Apr 2012 18:04:36 +0000 (20:04 +0200)
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 <hansmi@google.com>
Reviewed-by: Iustin Pop <iustin@google.com>

NEWS
lib/client/gnt_backup.py
lib/cmdlib.py
lib/constants.py
lib/query.py
man/gnt-backup.rst
qa/ganeti-qa.py
qa/qa_instance.py
qa/qa_utils.py
test/ganeti.query_unittest.py

diff --git a/NEWS b/NEWS
index d2973ff..8b90d0d 100644 (file)
--- 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
index 35190bb..edc8b44 100644 (file)
@@ -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,
index f4c6f93..db7685c 100644 (file)
@@ -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
index 040d186..68dc96c 100644 (file)
@@ -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([
index b2880a4..2a1213b 100644 (file)
@@ -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
index 4dd66df..da56299 100644 (file)
@@ -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
index 2926202..91cd506 100755 (executable)
@@ -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)
 
index dce7936..9834c75 100644 (file)
@@ -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."""
index 7bdd64f..f0ef63c 100644 (file)
@@ -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
index 9f93dc6..5e00d24 100755 (executable)
@@ -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"