commit f0b1bafeea8ce97a8970d4f054aff783331b8efa
Author: Iustin Pop <iustin@google.com>
Date:   Fri Feb 18 13:51:03 2011 +0100

    Change the list formatting to a 'special' chars
    
    And also enable verbose display via the, well, verbose option. Man
    page and tests are updated, and the formatting is moved from 4 if
    statements to a data structure.
    
    Signed-off-by: Iustin Pop <iustin@google.com>
    Reviewed-by: René Nussbaumer <rn@google.com>
    Reviewed-by: Michael Hanselmann <hansmi@google.com>

diff --git a/lib/cli.py b/lib/cli.py
index b1a7091..14acad9 100644
--- a/lib/cli.py
+++ b/lib/cli.py
@@ -2382,17 +2382,23 @@ class _QueryColumnFormatter:
   """Callable class for formatting fields of a query.
 
   """
-  def __init__(self, fn, status_fn):
+  def __init__(self, fn, status_fn, verbose):
     """Initializes this class.
 
     @type fn: callable
     @param fn: Formatting function
     @type status_fn: callable
     @param status_fn: Function to report fields' status
+    @type verbose: boolean
+    @param verbose: whether to use verbose field descriptions or not
 
     """
     self._fn = fn
     self._status_fn = status_fn
+    if verbose:
+      self._desc_index = 0
+    else:
+      self._desc_index = 1
 
   def __call__(self, data):
     """Returns a field's string representation.
@@ -2409,23 +2415,14 @@ class _QueryColumnFormatter:
     assert value is None, \
            "Found value %r for abnormal status %s" % (value, status)
 
-    if status == constants.RS_UNKNOWN:
-      return "(unknown)"
-
-    if status == constants.RS_NODATA:
-      return "(nodata)"
-
-    if status == constants.RS_UNAVAIL:
-      return "(unavail)"
-
-    if status == constants.RS_OFFLINE:
-      return "(offline)"
+    if status in constants.RSS_DESCRIPTION:
+      return constants.RSS_DESCRIPTION[status][self._desc_index]
 
     raise NotImplementedError("Unknown status %s" % status)
 
 
 def FormatQueryResult(result, unit=None, format_override=None, separator=None,
-                      header=False):
+                      header=False, verbose=False):
   """Formats data in L{objects.QueryResponse}.
 
   @type result: L{objects.QueryResponse}
@@ -2440,6 +2437,8 @@ def FormatQueryResult(result, unit=None, format_override=None, separator=None,
   @param separator: String used to separate fields
   @type header: bool
   @param header: Whether to output header row
+  @type verbose: boolean
+  @param verbose: whether to use verbose field descriptions or not
 
   """
   if unit is None:
@@ -2462,7 +2461,8 @@ def FormatQueryResult(result, unit=None, format_override=None, separator=None,
     assert fdef.title and fdef.name
     (fn, align_right) = _GetColumnFormatter(fdef, format_override, unit)
     columns.append(TableColumn(fdef.title,
-                               _QueryColumnFormatter(fn, _RecordStatus),
+                               _QueryColumnFormatter(fn, _RecordStatus,
+                                                     verbose),
                                align_right))
 
   table = FormatTable(result.data, columns, header, separator)
@@ -2511,7 +2511,7 @@ def _WarnUnknownFields(fdefs):
 
 
 def GenericList(resource, fields, names, unit, separator, header, cl=None,
-                format_override=None):
+                format_override=None, verbose=False):
   """Generic implementation for listing all items of a resource.
 
   @param resource: One of L{constants.QR_OP_LUXI}
@@ -2530,6 +2530,8 @@ def GenericList(resource, fields, names, unit, separator, header, cl=None,
   @type format_override: dict
   @param format_override: Dictionary for overriding field formatting functions,
     indexed by field name, contents like L{_DEFAULT_FORMAT_QUERY}
+  @type verbose: boolean
+  @param verbose: whether to use verbose field descriptions or not
 
   """
   if cl is None:
@@ -2544,7 +2546,8 @@ def GenericList(resource, fields, names, unit, separator, header, cl=None,
 
   (status, data) = FormatQueryResult(response, unit=unit, separator=separator,
                                      header=header,
-                                     format_override=format_override)
+                                     format_override=format_override,
+                                     verbose=verbose)
 
   for line in data:
     ToStdout(line)
diff --git a/lib/client/gnt_debug.py b/lib/client/gnt_debug.py
index 39ee043..e50eb0b 100644
--- a/lib/client/gnt_debug.py
+++ b/lib/client/gnt_debug.py
@@ -503,7 +503,7 @@ def ListLocks(opts, args): # pylint: disable-msg=W0613
   while True:
     ret = GenericList(constants.QR_LOCK, selected_fields, None, None,
                       opts.separator, not opts.no_headers,
-                      format_override=fmtoverride)
+                      format_override=fmtoverride, verbose=opts.verbose)
 
     if ret != constants.EXIT_SUCCESS:
       return ret
@@ -575,7 +575,8 @@ commands = {
     TestJobqueue, ARGS_NONE, [PRIORITY_OPT],
     "", "Test a few aspects of the job queue"),
   "locks": (
-    ListLocks, ARGS_NONE, [NOHDR_OPT, SEP_OPT, FIELDS_OPT, INTERVAL_OPT],
+    ListLocks, ARGS_NONE,
+    [NOHDR_OPT, SEP_OPT, FIELDS_OPT, INTERVAL_OPT, VERBOSE_OPT],
     "[--interval N]", "Show a list of locks in the master daemon"),
   }
 
diff --git a/lib/client/gnt_group.py b/lib/client/gnt_group.py
index 74fdcb4..9b11605 100644
--- a/lib/client/gnt_group.py
+++ b/lib/client/gnt_group.py
@@ -1,7 +1,7 @@
 #
 #
 
-# Copyright (C) 2010 Google Inc.
+# Copyright (C) 2010, 2011 Google Inc.
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -101,7 +101,7 @@ def ListGroups(opts, args):
 
   return GenericList(constants.QR_GROUP, desired_fields, args, None,
                      opts.separator, not opts.no_headers,
-                     format_override=fmtoverride)
+                     format_override=fmtoverride, verbose=opts.verbose)
 
 
 def ListGroupFields(opts, args):
@@ -189,7 +189,7 @@ commands = {
     "<group_name> <node>...", "Assign nodes to a group"),
   "list": (
     ListGroups, ARGS_MANY_GROUPS,
-    [NOHDR_OPT, SEP_OPT, FIELDS_OPT],
+    [NOHDR_OPT, SEP_OPT, FIELDS_OPT, VERBOSE_OPT],
     "[<group_name>...]",
     "Lists the node groups in the cluster. The available fields can be shown"
     " using the \"list-fields\" command (see the man page for details)."
diff --git a/lib/client/gnt_instance.py b/lib/client/gnt_instance.py
index d710c5a..e75fdaa 100644
--- a/lib/client/gnt_instance.py
+++ b/lib/client/gnt_instance.py
@@ -257,7 +257,7 @@ def ListInstances(opts, args):
 
   return GenericList(constants.QR_INSTANCE, selected_fields, args, opts.units,
                      opts.separator, not opts.no_headers,
-                     format_override=fmtoverride)
+                     format_override=fmtoverride, verbose=opts.verbose)
 
 
 def ListInstanceFields(opts, args):
@@ -1398,7 +1398,7 @@ commands = {
     "Show information on the specified instance(s)"),
   'list': (
     ListInstances, ARGS_MANY_INSTANCES,
-    [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT],
+    [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, VERBOSE_OPT],
     "[<instance>...]",
     "Lists the instances and their status. The available fields can be shown"
     " using the \"list-fields\" command (see the man page for details)."
diff --git a/lib/client/gnt_node.py b/lib/client/gnt_node.py
index 69d5cb7..6f9ccf1 100644
--- a/lib/client/gnt_node.py
+++ b/lib/client/gnt_node.py
@@ -223,7 +223,7 @@ def ListNodes(opts, args):
 
   return GenericList(constants.QR_NODE, selected_fields, args, opts.units,
                      opts.separator, not opts.no_headers,
-                     format_override=fmtoverride)
+                     format_override=fmtoverride, verbose=opts.verbose)
 
 
 def ListNodeFields(opts, args):
@@ -747,7 +747,7 @@ commands = {
     "[<node_name>...]", "Show information about the node(s)"),
   'list': (
     ListNodes, ARGS_MANY_NODES,
-    [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT],
+    [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, VERBOSE_OPT],
     "[nodes...]",
     "Lists the nodes in the cluster. The available fields can be shown using"
     " the \"list-fields\" command (see the man page for details)."
diff --git a/lib/constants.py b/lib/constants.py
index 6cec97d..2afa13d 100644
--- a/lib/constants.py
+++ b/lib/constants.py
@@ -1029,6 +1029,14 @@ RS_ALL = frozenset([
   RS_OFFLINE,
   ])
 
+#: Dictionary with special field cases and their verbose/terse formatting
+RSS_DESCRIPTION = {
+  RS_UNKNOWN: ("(unknown)", "??"),
+  RS_NODATA:  ("(nodata)",  "?"),
+  RS_OFFLINE: ("(offline)", "*"),
+  RS_UNAVAIL: ("(unavail)", "-"),
+  }
+
 # max dynamic devices
 MAX_NICS = 8
 MAX_DISKS = 16
diff --git a/man/ganeti.rst b/man/ganeti.rst
index 0bc1d02..a012231 100644
--- a/man/ganeti.rst
+++ b/man/ganeti.rst
@@ -179,3 +179,38 @@ Common daemon functionality
 
 All Ganeti daemons re-open the log file(s) when sent a SIGHUP signal.
 **logrotate**(8) can be used to rotate Ganeti's log files.
+
+Common field formatting
+-----------------------
+
+Multiple ganeti commands use the same framework for tabular listing of
+resources (e.g. **gnt-instance list**, **gnt-node list**, **gnt-group
+list**, **gnt-debug locks**, etc.). For these commands, special states
+are denoted via a special symbol (in terse mode) or a string (in
+verbose mode):
+
+*, (offline)
+    The node in question is marked offline, and thus it cannot be
+    queried for data. This result is persistent until the node is
+    de-offlined.
+
+?, (nodata)
+    Ganeti expected to receive an answer from this entity, but the
+    cluster RPC call failed and/or we didn't receive a valid answer;
+    usually more information is available in the node daemon log (if
+    the node is alive) or the master daemon log. This result is
+    transient, and re-running command might return a different result.
+
+-, (unavail)
+    The respective field doesn't make sense for this entity;
+    e.g. querying a down instance for its current memory 'live' usage,
+    or querying a non-vm_capable node for disk/memory data. This
+    result is persistent, and until the entity state is changed via
+    ganeti commands, the result won't change.
+
+??, (unknown)
+    This field is not known (note that this is different from entity
+    being unknown). Either you have mis-typed the field name, or you
+    are using a field that the running Ganeti master daemon doesn't
+    know. This result is persistent, re-running the command won't
+    change it.
diff --git a/man/gnt-debug.rst b/man/gnt-debug.rst
index d426b7d..d25beb8 100644
--- a/man/gnt-debug.rst
+++ b/man/gnt-debug.rst
@@ -93,7 +93,7 @@ failed jobs deliberately.
 LOCKS
 ~~~~~
 
-| **locks** [--no-headers] [--separator=*SEPARATOR*]
+| **locks** [--no-headers] [--separator=*SEPARATOR*] [-v]
 | [-o *[+]FIELD,...*] [--interval=*SECONDS*]
 
 Shows a list of locks in the master daemon.
@@ -103,6 +103,9 @@ The ``--no-headers`` option will skip the initial header line. The
 used between the output fields. Both these options are to help
 scripting.
 
+The ``-v`` option activates verbose mode, which changes the display of
+special field states (see **ganeti(7)**).
+
 The ``-o`` option takes a comma-separated list of output fields.
 The available fields and their meaning are:
 
diff --git a/man/gnt-group.rst b/man/gnt-group.rst
index 0a19425..c2e3f3a 100644
--- a/man/gnt-group.rst
+++ b/man/gnt-group.rst
@@ -92,7 +92,7 @@ least one group, so the last group cannot be removed.
 LIST
 ~~~~
 
-| **list** [--no-headers] [--separator=*SEPARATOR*]
+| **list** [--no-headers] [--separator=*SEPARATOR*] [-v]
 | [-o *[+]FIELD,...*] [group...]
 
 Lists all existing node groups in the cluster.
@@ -102,6 +102,9 @@ The ``--no-headers`` option will skip the initial header line. The
 used between the output fields. Both these options are to help
 scripting.
 
+The ``-v`` option activates verbose mode, which changes the display of
+special field states (see **ganeti(7)**).
+
 The ``-o`` option takes a comma-separated list of output fields.
 If the value of the option starts with the character ``+``, the new
 fields will be added to the default list. This allows to quickly
diff --git a/man/gnt-instance.rst b/man/gnt-instance.rst
index 46c31e3..788f259 100644
--- a/man/gnt-instance.rst
+++ b/man/gnt-instance.rst
@@ -624,7 +624,7 @@ LIST
 ^^^^
 
 | **list**
-| [--no-headers] [--separator=*SEPARATOR*] [--units=*UNITS*]
+| [--no-headers] [--separator=*SEPARATOR*] [--units=*UNITS*] [-v]
 | [-o *[+]FIELD,...*] [instance...]
 
 Shows the currently configured instances with memory usage, disk
@@ -642,11 +642,13 @@ option is given, then the values are shown in mebibytes to allow
 parsing by scripts. In both cases, the ``--units`` option can be
 used to enforce a given output unit.
 
+The ``-v`` option activates verbose mode, which changes the display of
+special field states (see **ganeti(7)**).
+
 The ``-o`` option takes a comma-separated list of output fields.
 The available fields and their meaning are:
 
 
-
 name
     the instance name
 
diff --git a/man/gnt-node.rst b/man/gnt-node.rst
index e8bddfc..97094cd 100644
--- a/man/gnt-node.rst
+++ b/man/gnt-node.rst
@@ -149,7 +149,7 @@ LIST
 
 | **list**
 | [--no-headers] [--separator=*SEPARATOR*]
-| [--units=*UNITS*] [-o *[+]FIELD,...*]
+| [--units=*UNITS*] [-v] [-o *[+]FIELD,...*]
 | [node...]
 
 Lists the nodes in the cluster.
@@ -169,11 +169,13 @@ used to enforce a given output unit.
 Queries of nodes will be done in parallel with any running jobs. This might
 give inconsistent results for the free disk/memory.
 
+The ``-v`` option activates verbose mode, which changes the display of
+special field states (see **ganeti(7)**).
+
 The ``-o`` option takes a comma-separated list of output fields.
 The available fields and their meaning are:
 
 
-
 name
     the node name
 
diff --git a/test/ganeti.cli_unittest.py b/test/ganeti.cli_unittest.py
index f06cc40..40446a7 100755
--- a/test/ganeti.cli_unittest.py
+++ b/test/ganeti.cli_unittest.py
@@ -1,7 +1,7 @@
 #!/usr/bin/python
 #
 
-# Copyright (C) 2008 Google Inc.
+# Copyright (C) 2008, 2011 Google Inc.
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -396,13 +396,21 @@ class TestFormatQueryResult(unittest.TestCase):
       ])
 
     self.assertEqual(cli.FormatQueryResult(response, header=True,
-                                           separator="|"),
+                                           separator="|", verbose=True),
       (cli.QR_UNKNOWN, [
       "ID|unk|Unavail|NoData|OffLine",
       "1|(unknown)|N||(offline)",
       "2|(unknown)|(nodata)|x|(offline)",
       "3|(unknown)|N|(unavail)|(offline)",
       ]))
+    self.assertEqual(cli.FormatQueryResult(response, header=True,
+                                           separator="|", verbose=False),
+      (cli.QR_UNKNOWN, [
+      "ID|unk|Unavail|NoData|OffLine",
+      "1|??|N||*",
+      "2|??|?|x|*",
+      "3|??|N|-|*",
+      ]))
 
   def testNoData(self):
     fields = [
@@ -452,12 +460,19 @@ class TestFormatQueryResult(unittest.TestCase):
       ])
 
     self.assertEqual(cli.FormatQueryResult(response, header=False,
-                                           separator="|"),
+                                           separator="|", verbose=True),
       (cli.QR_INCOMPLETE, [
       "1|N||(offline)",
       "2|(nodata)|x|abc",
       "3|N|(unavail)|(offline)",
       ]))
+    self.assertEqual(cli.FormatQueryResult(response, header=False,
+                                           separator="|", verbose=False),
+      (cli.QR_INCOMPLETE, [
+      "1|N||*",
+      "2|?|x|abc",
+      "3|N|-|*",
+      ]))
 
   def testInvalidFieldType(self):
     fields = [
