On Mon, Dec 13, 2010 at 4:26 PM, Michael Hanselmann <[email protected]> wrote:
> +def FormatQueryResult(result, unit=None, format_override=None, 
> separator=None,
> +                      header=False):
> +  """Formats data in L{objects.QueryResponse}.
> +
> + �...@type result: L{objects.QueryResponse}
> + �...@param result: result of query operation
> + �...@type unit: string
> + �...@param unit: Unit used for formatting fields of type 
> L{constants.QFT_UNIT}
> + �...@type format_override: dict
> + �...@param format_override: Dictionary for overriding field formatting 
> functions,
> +    indexed by field name, contents like L{_DEFAULT_FORMAT_QUERY}
> + �...@type separator: string or None
> + �...@param separator: String used to separate fields
> + �...@type header: bool
> + �...@param header: Whether to output header row
> +
> +  """
> +  if unit is None:
> +    if separator:
> +      unit = "m"
> +    else:
> +      unit = "h"

Maybe describe the units. What does this short-hand characters stand
for? What other units are available?

> +
> +  if format_override is None:
> +    format_override = {}
> +
> +  stats = dict.fromkeys(constants.QRFS_ALL, 0)
> +
> +  def _RecordStatus(status):
> +    if status in stats:
> +      stats[status] += 1
> +
> +  columns = []
> +  for fdef in result.fields:
> +    assert fdef.title and fdef.name
> +    (fn, align_right) = _GetColumnFormatter(fdef, format_override, unit)
> +    columns.append(TableColumn(fdef.title,
> +                               _QueryColumnFormatter(fn, _RecordStatus),
> +                               align_right))
> +
> +  table = FormatTable(result.data, columns, header, separator)
> +
> +  # Collect statistics
> +  assert len(stats) == len(constants.QRFS_ALL)
> +  assert compat.all(count >= 0 for count in stats.values())
> +
> +  # Determine overall status. If there was no data, unknown fields must be
> +  # detected via the field definitions.
> +  if (stats[constants.QRFS_UNKNOWN] or
> +      (not result.data and _GetUnknownFields(result.fields))):
> +    status = QR_UNKNOWN
> +  elif compat.any(count > 0 for key, count in stats.items()
> +                  if key != constants.QRFS_NORMAL):
> +    status = QR_INCOMPLETE
> +  else:
> +    status = QR_NORMAL
> +
> +  return (status, table)
> +
> +
> +def _GetUnknownFields(fdefs):
> +  """Returns list of unknown fields included in C{fdefs}.
> +
> + �...@type fdefs: list of L{objects.QueryFieldDefinition}
> +
> +  """
> +  return [fdef for fdef in fdefs
> +          if fdef.kind == constants.QFT_UNKNOWN]
> +
> +
> +def _WarnUnknownFields(fdefs):
> +  """Prints a warning to stderr if a query included unknown fields.
> +
> + �...@type fdefs: list of L{objects.QueryFieldDefinition}
> +
> +  """
> +  unknown = _GetUnknownFields(fdefs)
> +  if unknown:
> +    ToStderr("Warning: Queried for unknown fields %s",
> +             utils.CommaJoin(fdef.name for fdef in unknown))
> +    return True
> +
> +  return False
> +
> +
> +def GenericList(resource, fields, names, unit, separator, header, cl=None,
> +                format_override=None):
> +  """Generic implementation for listing all items of a resource.
> +
> + �...@param resource: One of L{constants.QR_OP_LUXI}
> + �...@type fields: list of strings
> + �...@param fields: List of fields to query for
> + �...@type names: list of strings
> + �...@param names: Names of items to query for
> + �...@type unit: string or None
> + �...@param unit: Unit used for formatting fields of type 
> L{constants.QFT_UNIT} or
> +    None for automatic choice (human-readable for non-separator usage,
> +    otherwise megabytes); this is a one-letter string
> + �...@type separator: string or None
> + �...@param separator: String used to separate fields
> + �...@type header: bool
> + �...@param header: Whether to show header row
> + �...@type format_override: dict
> + �...@param format_override: Dictionary for overriding field formatting 
> functions,
> +    indexed by field name, contents like L{_DEFAULT_FORMAT_QUERY}
> +
> +  """
> +  if cl is None:
> +    cl = GetClient()
> +
> +  if not names:
> +    names = None
> +
> +  response = cl.Query(resource, fields, qlang.MakeSimpleFilter("name", 
> names))
> +
> +  found_unknown = _WarnUnknownFields(response.fields)
> +
> +  (status, data) = FormatQueryResult(response, unit=unit, 
> separator=separator,
> +                                     header=header,
> +                                     format_override=format_override)
> +
> +  for line in data:
> +    ToStdout(line)
> +
> +  assert ((found_unknown and status == QR_UNKNOWN) or
> +          (not found_unknown and status != QR_UNKNOWN))
> +
> +  if status == QR_UNKNOWN:
> +    return constants.EXIT_UNKNOWN_FIELD
> +
> +  # TODO: Should the list command fail if not all data could be collected?
> +  return constants.EXIT_SUCCESS
> +
> +
> +def GenericListFields(resource, fields, separator, header, cl=None):
> +  """Generic implementation for listing fields for a resource.
> +
> + �...@param resource: One of L{constants.QR_OP_LUXI}
> + �...@type fields: list of strings
> + �...@param fields: List of fields to query for
> + �...@type separator: string or None
> + �...@param separator: String used to separate fields
> + �...@type header: bool
> + �...@param header: Whether to show header row
> +
> +  """
> +  if cl is None:
> +    cl = GetClient()
> +
> +  if not fields:
> +    fields = None
> +
> +  response = cl.QueryFields(resource, fields)
> +
> +  found_unknown = _WarnUnknownFields(response.fields)
> +
> +  columns = [
> +    TableColumn("Name", str, False),
> +    TableColumn("Title", str, False),
> +    # TODO: Add field description to master daemon
> +    ]
> +
> +  rows = [[fdef.name, fdef.title] for fdef in response.fields]
> +
> +  for line in FormatTable(rows, columns, header, separator):
> +    ToStdout(line)
> +
> +  if found_unknown:
> +    return constants.EXIT_UNKNOWN_FIELD
> +
> +  return constants.EXIT_SUCCESS
> +
> +
> +class TableColumn:
> +  """Describes a column for L{FormatTable}.
> +
> +  """
> +  def __init__(self, title, fn, align_right):
> +    """Initializes this class.
> +
> +   �...@type title: string
> +   �...@param title: Column title
> +   �...@type fn: callable
> +   �...@param fn: Formatting function
> +   �...@type align_right: bool
> +   �...@param align_right: Whether to align values on the right-hand side
> +
> +    """
> +    self.title = title
> +    self.format = fn
> +    self.align_right = align_right
> +
> +
> +def _GetColFormatString(width, align_right):
> +  """Returns the format string for a field.
> +
> +  """
> +  if align_right:
> +    sign = ""
> +  else:
> +    sign = "-"
> +
> +  return "%%%s%ss" % (sign, width)
> +
> +
> +def FormatTable(rows, columns, header, separator):
> +  """Formats data as a table.
> +
> + �...@type rows: list of lists
> + �...@param rows: Row data, one list per row
> + �...@type columns: list of L{TableColumn}
> + �...@param columns: Column descriptions
> + �...@type header: bool
> + �...@param header: Whether to show header row
> + �...@type separator: string or None
> + �...@param separator: String used to separate columns
> +
> +  """
> +  if header:
> +    data = [[col.title for col in columns]]
> +    colwidth = [len(col.title) for col in columns]
> +  else:
> +    data = []
> +    colwidth = [0 for _ in columns]
> +
> +  # Format row data
> +  for row in rows:
> +    assert len(row) == len(columns)
> +
> +    formatted = [col.format(value) for value, col in zip(row, columns)]
> +
> +    if separator is None:
> +      # Update column widths
> +      for idx, (oldwidth, value) in enumerate(zip(colwidth, formatted)):
> +        # Modifying a list's items while iterating is fine
> +        colwidth[idx] = max(oldwidth, len(value))
> +
> +    data.append(formatted)
> +
> +  if separator is not None:
> +    # Return early if a separator is used
> +    return [separator.join(row) for row in data]
> +
> +  if columns and not columns[-1].align_right:
> +    # Avoid unnecessary spaces at end of line
> +    colwidth[-1] = 0
> +
> +  # Build format string
> +  fmt = " ".join([_GetColFormatString(width, col.align_right)
> +                  for col, width in zip(columns, colwidth)])
> +
> +  return [fmt % tuple(row) for row in data]
> +
> +
>  def FormatTimestamp(ts):
>   """Formats a given timestamp.
>
> diff --git a/lib/constants.py b/lib/constants.py
> index 184419b..9a979f8 100644
> --- a/lib/constants.py
> +++ b/lib/constants.py
> @@ -452,6 +452,9 @@ EXIT_NOTMASTER = 11
>  EXIT_NODESETUP_ERROR = 12
>  EXIT_CONFIRMATION = 13 # need user confirmation
>
> +#: Exit code for query operations with unknown fields
> +EXIT_UNKNOWN_FIELD = 14
> +
>  # tags
>  TAG_CLUSTER = "cluster"
>  TAG_NODE = "node"
> @@ -981,6 +984,13 @@ QRFS_NODATA = 2
>  #: Value unavailable for item
>  QRFS_UNAVAIL = 3
>
> +QRFS_ALL = frozenset([
> +  QRFS_NORMAL,
> +  QRFS_UNKNOWN,
> +  QRFS_NODATA,
> +  QRFS_UNAVAIL,
> +  ])
> +
>  # max dynamic devices
>  MAX_NICS = 8
>  MAX_DISKS = 16
> diff --git a/test/ganeti.cli_unittest.py b/test/ganeti.cli_unittest.py
> index 64e3ddb..0e76e83 100755
> --- a/test/ganeti.cli_unittest.py
> +++ b/test/ganeti.cli_unittest.py
> @@ -31,6 +31,7 @@ from ganeti import constants
>  from ganeti import cli
>  from ganeti import errors
>  from ganeti import utils
> +from ganeti import objects
>  from ganeti.errors import OpPrereqError, ParameterError
>
>
> @@ -248,6 +249,241 @@ class TestGenerateTable(unittest.TestCase):
>                None, None, "m", exp)
>
>
> +class TestFormatQueryResult(unittest.TestCase):
> +  def test(self):
> +    fields = [
> +      objects.QueryFieldDefinition(name="name", title="Name",
> +                                   kind=constants.QFT_TEXT),
> +      objects.QueryFieldDefinition(name="size", title="Size",
> +                                   kind=constants.QFT_NUMBER),
> +      objects.QueryFieldDefinition(name="act", title="Active",
> +                                   kind=constants.QFT_BOOL),
> +      objects.QueryFieldDefinition(name="mem", title="Memory",
> +                                   kind=constants.QFT_UNIT),
> +      objects.QueryFieldDefinition(name="other", title="SomeList",
> +                                   kind=constants.QFT_OTHER),
> +      ]
> +
> +    response = objects.QueryResponse(fields=fields, data=[
> +      [(constants.QRFS_NORMAL, "nodeA"), (constants.QRFS_NORMAL, 128),
> +       (constants.QRFS_NORMAL, False), (constants.QRFS_NORMAL, 1468006),
> +       (constants.QRFS_NORMAL, [])],
> +      [(constants.QRFS_NORMAL, "other"), (constants.QRFS_NORMAL, 512),
> +       (constants.QRFS_NORMAL, True), (constants.QRFS_NORMAL, 16),
> +       (constants.QRFS_NORMAL, [1, 2, 3])],
> +      [(constants.QRFS_NORMAL, "xyz"), (constants.QRFS_NORMAL, 1024),
> +       (constants.QRFS_NORMAL, True), (constants.QRFS_NORMAL, 4096),
> +       (constants.QRFS_NORMAL, [{}, {}])],
> +      ])
> +
> +    self.assertEqual(cli.FormatQueryResult(response, unit="h", header=True),
> +      (cli.QR_NORMAL, [
> +      "Name  Size Active Memory SomeList",
> +      "nodeA  128 N        1.4T []",
> +      "other  512 Y         16M [1, 2, 3]",
> +      "xyz   1024 Y        4.0G [{}, {}]",
> +      ]))
> +
> +  def testTimestampAndUnit(self):
> +    fields = [
> +      objects.QueryFieldDefinition(name="name", title="Name",
> +                                   kind=constants.QFT_TEXT),
> +      objects.QueryFieldDefinition(name="size", title="Size",
> +                                   kind=constants.QFT_UNIT),
> +      objects.QueryFieldDefinition(name="mtime", title="ModTime",
> +                                   kind=constants.QFT_TIMESTAMP),
> +      ]
> +
> +    response = objects.QueryResponse(fields=fields, data=[
> +      [(constants.QRFS_NORMAL, "a"), (constants.QRFS_NORMAL, 1024),
> +       (constants.QRFS_NORMAL, 0)],
> +      [(constants.QRFS_NORMAL, "b"), (constants.QRFS_NORMAL, 144996),
> +       (constants.QRFS_NORMAL, 1291746295)],
> +      ])
> +
> +    self.assertEqual(cli.FormatQueryResult(response, unit="m", header=True),
> +      (cli.QR_NORMAL, [
> +      "Name   Size ModTime",
> +      "a      1024 %s" % utils.FormatTime(0),
> +      "b    144996 %s" % utils.FormatTime(1291746295),
> +      ]))
> +
> +  def testOverride(self):
> +    fields = [
> +      objects.QueryFieldDefinition(name="name", title="Name",
> +                                   kind=constants.QFT_TEXT),
> +      objects.QueryFieldDefinition(name="cust", title="Custom",
> +                                   kind=constants.QFT_OTHER),
> +      objects.QueryFieldDefinition(name="xt", title="XTime",
> +                                   kind=constants.QFT_TIMESTAMP),
> +      ]
> +
> +    response = objects.QueryResponse(fields=fields, data=[
> +      [(constants.QRFS_NORMAL, "x"), (constants.QRFS_NORMAL, ["a", "b", 
> "c"]),
> +       (constants.QRFS_NORMAL, 1234)],
> +      [(constants.QRFS_NORMAL, "y"), (constants.QRFS_NORMAL, range(10)),
> +       (constants.QRFS_NORMAL, 1291746295)],
> +      ])
> +
> +    override = {
> +      "cust": (utils.CommaJoin, False),
> +      "xt": (hex, True),
> +      }
> +
> +    self.assertEqual(cli.FormatQueryResult(response, unit="h", header=True,
> +                                           format_override=override),
> +      (cli.QR_NORMAL, [
> +      "Name Custom                            XTime",
> +      "x    a, b, c                           0x4d2",
> +      "y    0, 1, 2, 3, 4, 5, 6, 7, 8, 9 0x4cfe7bf7",
> +      ]))
> +
> +  def testSeparator(self):
> +    fields = [
> +      objects.QueryFieldDefinition(name="name", title="Name",
> +                                   kind=constants.QFT_TEXT),
> +      objects.QueryFieldDefinition(name="count", title="Count",
> +                                   kind=constants.QFT_NUMBER),
> +      objects.QueryFieldDefinition(name="desc", title="Description",
> +                                   kind=constants.QFT_TEXT),
> +      ]
> +
> +    response = objects.QueryResponse(fields=fields, data=[
> +      [(constants.QRFS_NORMAL, "instance1.example.com"),
> +       (constants.QRFS_NORMAL, 21125), (constants.QRFS_NORMAL, "Hello 
> World!")],
> +      [(constants.QRFS_NORMAL, "mail.other.net"),
> +       (constants.QRFS_NORMAL, -9000), (constants.QRFS_NORMAL, "a,b,c")],
> +      ])
> +
> +    for sep in [":", "|", "#", "|||", "###", "@@@", "@#@"]:
> +      for header in [None, "Name%sCount%sDescription" % (sep, sep)]:
> +        exp = []
> +        if header:
> +          exp.append(header)
> +        exp.extend([
> +          "instance1.example.com%s21125%sHello World!" % (sep, sep),
> +          "mail.other.net%s-9000%sa,b,c" % (sep, sep),
> +          ])
> +
> +        self.assertEqual(cli.FormatQueryResult(response, separator=sep,
> +                                               header=bool(header)),
> +                         (cli.QR_NORMAL, exp))
> +
> +  def testStatusWithUnknown(self):
> +    fields = [
> +      objects.QueryFieldDefinition(name="id", title="ID",
> +                                   kind=constants.QFT_NUMBER),
> +      objects.QueryFieldDefinition(name="unk", title="unk",
> +                                   kind=constants.QFT_UNKNOWN),
> +      objects.QueryFieldDefinition(name="unavail", title="Unavail",
> +                                   kind=constants.QFT_BOOL),
> +      objects.QueryFieldDefinition(name="nodata", title="NoData",
> +                                   kind=constants.QFT_TEXT),
> +      ]
> +
> +    response = objects.QueryResponse(fields=fields, data=[
> +      [(constants.QRFS_NORMAL, 1), (constants.QRFS_UNKNOWN, None),
> +       (constants.QRFS_NORMAL, False), (constants.QRFS_NORMAL, "")],
> +      [(constants.QRFS_NORMAL, 2), (constants.QRFS_UNKNOWN, None),
> +       (constants.QRFS_NODATA, None), (constants.QRFS_NORMAL, "x")],
> +      [(constants.QRFS_NORMAL, 3), (constants.QRFS_UNKNOWN, None),
> +       (constants.QRFS_NORMAL, False), (constants.QRFS_UNAVAIL, None)],
> +      ])
> +
> +    self.assertEqual(cli.FormatQueryResult(response, header=True,
> +                                           separator="|"),
> +      (cli.QR_UNKNOWN, [
> +      "ID|unk|Unavail|NoData",
> +      "1|<unknown>|N|",
> +      "2|<unknown>|<nodata>|x",
> +      "3|<unknown>|N|<unavail>",
> +      ]))
> +
> +  def testNoData(self):
> +    fields = [
> +      objects.QueryFieldDefinition(name="id", title="ID",
> +                                   kind=constants.QFT_NUMBER),
> +      objects.QueryFieldDefinition(name="name", title="Name",
> +                                   kind=constants.QFT_TEXT),
> +      ]
> +
> +    response = objects.QueryResponse(fields=fields, data=[])
> +
> +    self.assertEqual(cli.FormatQueryResult(response, header=True),
> +                     (cli.QR_NORMAL, ["ID Name"]))
> +
> +  def testNoDataWithUnknown(self):
> +    fields = [
> +      objects.QueryFieldDefinition(name="id", title="ID",
> +                                   kind=constants.QFT_NUMBER),
> +      objects.QueryFieldDefinition(name="unk", title="unk",
> +                                   kind=constants.QFT_UNKNOWN),
> +      ]
> +
> +    response = objects.QueryResponse(fields=fields, data=[])
> +
> +    self.assertEqual(cli.FormatQueryResult(response, header=False),
> +                     (cli.QR_UNKNOWN, []))
> +
> +  def testStatus(self):
> +    fields = [
> +      objects.QueryFieldDefinition(name="id", title="ID",
> +                                   kind=constants.QFT_NUMBER),
> +      objects.QueryFieldDefinition(name="unavail", title="Unavail",
> +                                   kind=constants.QFT_BOOL),
> +      objects.QueryFieldDefinition(name="nodata", title="NoData",
> +                                   kind=constants.QFT_TEXT),
> +      ]
> +
> +    response = objects.QueryResponse(fields=fields, data=[
> +      [(constants.QRFS_NORMAL, 1), (constants.QRFS_NORMAL, False),
> +       (constants.QRFS_NORMAL, "")],
> +      [(constants.QRFS_NORMAL, 2), (constants.QRFS_NODATA, None),
> +       (constants.QRFS_NORMAL, "x")],
> +      [(constants.QRFS_NORMAL, 3), (constants.QRFS_NORMAL, False),
> +       (constants.QRFS_UNAVAIL, None)],
> +      ])
> +
> +    self.assertEqual(cli.FormatQueryResult(response, header=False,
> +                                           separator="|"),
> +      (cli.QR_INCOMPLETE, [
> +      "1|N|",
> +      "2|<nodata>|x",
> +      "3|N|<unavail>",
> +      ]))
> +
> +  def testInvalidFieldType(self):
> +    fields = [
> +      objects.QueryFieldDefinition(name="x", title="x",
> +                                   kind="#some#other#type"),
> +      ]
> +
> +    response = objects.QueryResponse(fields=fields, data=[])
> +
> +    self.assertRaises(NotImplementedError, cli.FormatQueryResult, response)
> +
> +  def testInvalidFieldStatus(self):
> +    fields = [
> +      objects.QueryFieldDefinition(name="x", title="x",
> +                                   kind=constants.QFT_TEXT),
> +      ]
> +
> +    response = objects.QueryResponse(fields=fields, data=[[(-1, None)]])
> +    self.assertRaises(NotImplementedError, cli.FormatQueryResult, response)
> +
> +    response = objects.QueryResponse(fields=fields, data=[[(-1, "x")]])
> +    self.assertRaises(AssertionError, cli.FormatQueryResult, response)
> +
> +  def testEmptyFieldTitle(self):
> +    fields = [
> +      objects.QueryFieldDefinition(name="x", title="",
> +                                   kind=constants.QFT_TEXT),
> +      ]
> +
> +    response = objects.QueryResponse(fields=fields, data=[])
> +    self.assertRaises(AssertionError, cli.FormatQueryResult, response)
> +
> +
>  class _MockJobPollCb(cli.JobPollCbBase, cli.JobPollReportCbBase):
>   def __init__(self, tc, job_id):
>     self.tc = tc
> --
> 1.7.3.1

LGTM

>
>

Reply via email to