commit dcb939711bb4ed751a71f522e41c9af986bfe96e
Author: Michael Hanselmann <hansmi@google.com>
Date:   Tue Jul 24 11:45:11 2007 +0000

    - Implement “gnt-node volumes”
    - Create all --output options using a constant
    - Put node checking code from opcodes into a single function
    - Do the same for output fields
    
    Reviewed-by: iustinp

diff --git a/daemons/ganeti-noded b/daemons/ganeti-noded
index 9c09140..60b7704 100755
--- a/daemons/ganeti-noded
+++ b/daemons/ganeti-noded
@@ -235,6 +235,9 @@ class ServerObject(pb.Avatar):
   def perspective_node_leave_cluster(self, params):
     return backend.LeaveCluster()
 
+  def perspective_node_volumes(self, params):
+    return backend.NodeVolumes()
+
   # cluster --------------------------
 
   def perspective_version(self,params):
diff --git a/lib/backend.py b/lib/backend.py
index d57f3eb..3064f21 100644
--- a/lib/backend.py
+++ b/lib/backend.py
@@ -260,6 +260,35 @@ def ListVolumeGroups():
   return utils.ListVolumeGroups()
 
 
+def NodeVolumes():
+  """List all volumes on this node.
+
+  """
+  result = utils.RunCmd(["lvs", "--noheadings", "--units=m", "--nosuffix",
+                         "--separator=|",
+                         "--options=lv_name,lv_size,devices,vg_name"])
+  if result.failed:
+    logger.Error("Failed to list logical volumes, lvs output: %s" %
+                 result.output)
+    return {}
+
+  def parse_dev(dev):
+    if '(' in dev:
+      return dev.split('(')[0]
+    else:
+      return dev
+
+  def map_line(line):
+    return {
+      'name': line[0].strip(),
+      'size': line[1].strip(),
+      'dev': parse_dev(line[2].strip()),
+      'vg': line[3].strip(),
+    }
+
+  return [map_line(line.split('|')) for line in result.output.splitlines()]
+
+
 def BridgesExist(bridges_list):
   """Check if a list of bridges exist on the current node
 
diff --git a/lib/cli.py b/lib/cli.py
index 56ec477..5508fbd 100644
--- a/lib/cli.py
+++ b/lib/cli.py
@@ -39,7 +39,7 @@ from optparse import (OptionParser, make_option, TitledHelpFormatter,
 __all__ = ["DEBUG_OPT", "NOHDR_OPT", "SEP_OPT", "GenericMain", "SubmitOpCode",
            "cli_option",
            "ARGS_NONE", "ARGS_FIXED", "ARGS_ATLEAST", "ARGS_ANY", "ARGS_ONE",
-           "USEUNITS_OPT"]
+           "USEUNITS_OPT", "FIELDS_OPT"]
 
 DEBUG_OPT = make_option("-d", "--debug", default=False,
                         action="store_true",
@@ -58,6 +58,10 @@ USEUNITS_OPT = make_option("--human-readable", default=False,
                            action="store_true", dest="human_readable",
                            help="Print sizes in human readable format")
 
+FIELDS_OPT = make_option("-o", "--output", dest="output", action="store",
+                         type="string", help="Select output fields",
+                         metavar="FIELDS")
+
 _LOCK_OPT = make_option("--lock-retries", default=None,
                         type="int", help=SUPPRESS_HELP)
 
diff --git a/lib/cmdlib.py b/lib/cmdlib.py
index e7acceb..a4beb2d 100644
--- a/lib/cmdlib.py
+++ b/lib/cmdlib.py
@@ -164,6 +164,36 @@ class NoHooksLU(LogicalUnit):
     return
 
 
+def _GetWantedNodes(lu, nodes):
+  if nodes is not None and not isinstance(nodes, list):
+    raise errors.OpPrereqError, "Invalid argument type 'nodes'"
+
+  if nodes:
+    wanted_nodes = []
+
+    for name in nodes:
+      node = lu.cfg.GetNodeInfo(lu.cfg.ExpandNodeName(name))
+      if node is None:
+        raise errors.OpPrereqError, ("No such node name '%s'" % name)
+    wanted_nodes.append(node)
+
+    return wanted_nodes
+  else:
+    return [lu.cfg.GetNodeInfo(name) for name in lu.cfg.GetNodeList()]
+
+
+def _CheckOutputFields(static, dynamic, selected):
+    static_fields = frozenset(static)
+    dynamic_fields = frozenset(dynamic)
+
+    all_fields = static_fields | dynamic_fields
+
+    if not all_fields.issuperset(selected):
+      raise errors.OpPrereqError, ("Unknown output fields selected: %s"
+                                   % ",".join(frozenset(selected).
+                                              difference(all_fields)))
+
+
 def _UpdateEtcHosts(fullnode, ip):
   """Ensure a node has a correct entry in /etc/hosts.
 
@@ -1028,15 +1058,12 @@ class LUQueryNodes(NoHooksLU):
     This checks that the fields required are valid output fields.
 
     """
-    self.static_fields = frozenset(["name", "pinst", "sinst", "pip", "sip"])
     self.dynamic_fields = frozenset(["dtotal", "dfree",
                                      "mtotal", "mnode", "mfree"])
-    self.all_fields = self.static_fields | self.dynamic_fields
 
-    if not self.all_fields.issuperset(self.op.output_fields):
-      raise errors.OpPrereqError, ("Unknown output fields selected: %s"
-                                   % ",".join(frozenset(self.op.output_fields).
-                                              difference(self.all_fields)))
+    _CheckOutputFields(static=["name", "pinst", "sinst", "pip", "sip"],
+                       dynamic=self.dynamic_fields,
+                       selected=self.op.output_fields)
 
 
   def Exec(self, feedback_fn):
@@ -1106,6 +1133,73 @@ class LUQueryNodes(NoHooksLU):
     return output
 
 
+class LUQueryNodeVolumes(NoHooksLU):
+  """Logical unit for getting volumes on node(s).
+
+  """
+  _OP_REQP = ["nodes", "output_fields"]
+
+  def CheckPrereq(self):
+    """Check prerequisites.
+
+    This checks that the fields required are valid output fields.
+
+    """
+    self.nodes = _GetWantedNodes(self, self.op.nodes)
+
+    _CheckOutputFields(static=["node"],
+                       dynamic=["phys", "vg", "name", "size", "instance"],
+                       selected=self.op.output_fields)
+
+
+  def Exec(self, feedback_fn):
+    """Computes the list of nodes and their attributes.
+
+    """
+    nodenames = utils.NiceSort([node.name for node in self.nodes])
+    volumes = rpc.call_node_volumes(nodenames)
+
+    ilist = [self.cfg.GetInstanceInfo(iname) for iname
+             in self.cfg.GetInstanceList()]
+
+    lv_by_node = dict([(inst, inst.MapLVsByNode()) for inst in ilist])
+
+    output = []
+    for node in nodenames:
+      node_vols = volumes[node][:]
+      node_vols.sort(key=lambda vol: vol['dev'])
+
+      for vol in node_vols:
+        node_output = []
+        for field in self.op.output_fields:
+          if field == "node":
+            val = node
+          elif field == "phys":
+            val = vol['dev']
+          elif field == "vg":
+            val = vol['vg']
+          elif field == "name":
+            val = vol['name']
+          elif field == "size":
+            val = int(float(vol['size']))
+          elif field == "instance":
+            for inst in ilist:
+              if node not in lv_by_node[inst]:
+                continue
+              if vol['name'] in lv_by_node[inst][node]:
+                val = inst.name
+                break
+            else:
+              val = '-'
+          else:
+            raise errors.ParameterError, field
+          node_output.append(str(val))
+
+        output.append(node_output)
+
+    return output
+
+
 def _CheckNodesDirs(node_list, paths):
   """Verify if the given nodes have the same files.
 
@@ -1477,16 +1571,8 @@ class LUClusterCopyFile(NoHooksLU):
     """
     if not os.path.exists(self.op.filename):
       raise errors.OpPrereqError("No such filename '%s'" % self.op.filename)
-    if self.op.nodes:
-      nodes = self.op.nodes
-    else:
-      nodes = self.cfg.GetNodeList()
-    self.nodes = []
-    for node in nodes:
-      nname = self.cfg.ExpandNodeName(node)
-      if nname is None:
-        raise errors.OpPrereqError, ("Node '%s' is unknown." % node)
-      self.nodes.append(nname)
+
+    self.nodes = _GetWantedNodes(self, self.op.nodes)
 
   def Exec(self, feedback_fn):
     """Copy a file from master to some nodes.
@@ -1540,16 +1626,7 @@ class LURunClusterCommand(NoHooksLU):
     It checks that the given list of nodes is valid.
 
     """
-    if self.op.nodes:
-      nodes = self.op.nodes
-    else:
-      nodes = self.cfg.GetNodeList()
-    self.nodes = []
-    for node in nodes:
-      nname = self.cfg.ExpandNodeName(node)
-      if nname is None:
-        raise errors.OpPrereqError, ("Node '%s' is unknown." % node)
-      self.nodes.append(nname)
+    self.nodes = _GetWantedNodes(self, self.op.nodes)
 
   def Exec(self, feedback_fn):
     """Run a command on some nodes.
@@ -1557,8 +1634,8 @@ class LURunClusterCommand(NoHooksLU):
     """
     data = []
     for node in self.nodes:
-      result = utils.RunCmd(["ssh", node, self.op.command])
-      data.append((node, result.cmd, result.output, result.exit_code))
+      result = utils.RunCmd(["ssh", node.name, self.op.command])
+      data.append((node.name, result.cmd, result.output, result.exit_code))
 
     return data
 
@@ -1884,7 +1961,7 @@ class LUQueryInstances(NoHooksLU):
   """Logical unit for querying instances.
 
   """
-  OP_REQP = ["output_fields"]
+  _OP_REQP = ["output_fields"]
 
   def CheckPrereq(self):
     """Check prerequisites.
@@ -1892,17 +1969,12 @@ class LUQueryInstances(NoHooksLU):
     This checks that the fields required are valid output fields.
 
     """
-
-    self.static_fields = frozenset(["name", "os", "pnode", "snodes",
-                                    "admin_state", "admin_ram",
-                                    "disk_template", "ip", "mac", "bridge"])
     self.dynamic_fields = frozenset(["oper_state", "oper_ram"])
-    self.all_fields = self.static_fields | self.dynamic_fields
-
-    if not self.all_fields.issuperset(self.op.output_fields):
-      raise errors.OpPrereqError, ("Unknown output fields selected: %s"
-                                   % ",".join(frozenset(self.op.output_fields).
-                                              difference(self.all_fields)))
+    _CheckOutputFields(static=["name", "os", "pnode", "snodes",
+                               "admin_state", "admin_ram",
+                               "disk_template", "ip", "mac", "bridge"],
+                       dynamic=self.dynamic_fields,
+                       selected=self.op.output_fields)
 
   def Exec(self, feedback_fn):
     """Computes the list of nodes and their attributes.
@@ -3074,20 +3146,7 @@ class LUQueryNodeData(NoHooksLU):
     This only checks the optional node list against the existing names.
 
     """
-    if not isinstance(self.op.nodes, list):
-      raise errors.OpPrereqError, "Invalid argument type 'nodes'"
-    if self.op.nodes:
-      self.wanted_nodes = []
-      names = self.op.nodes
-      for name in names:
-        node = self.cfg.GetNodeInfo(self.cfg.ExpandNodeName(name))
-        if node is None:
-          raise errors.OpPrereqError, ("No such node name '%s'" % name)
-      self.wanted_nodes.append(node)
-    else:
-      self.wanted_nodes = [self.cfg.GetNodeInfo(name) for name
-                           in self.cfg.GetNodeList()]
-    return
+    self.wanted_nodes = _GetWantedNodes(self, self.op.nodes)
 
   def Exec(self, feedback_fn):
     """Compute and return the list of nodes.
@@ -3214,18 +3273,9 @@ class LUQueryExports(NoHooksLU):
     """Check that the nodelist contains only existing nodes.
 
     """
-    nodes = getattr(self.op, "nodes", None)
-    if not nodes:
-      self.op.nodes = self.cfg.GetNodeList()
-    else:
-      expnodes = [self.cfg.ExpandNodeName(node) for node in nodes]
-      if expnodes.count(None) > 0:
-        raise errors.OpPrereqError, ("At least one of the given nodes %s"
-                                     " is unknown" % self.op.nodes)
-      self.op.nodes = expnodes
+    self.nodes = _GetWantedNodes(self, getattr(self.op, "nodes", None))
 
   def Exec(self, feedback_fn):
-
     """Compute the list of all the exported system images.
 
     Returns:
@@ -3234,7 +3284,7 @@ class LUQueryExports(NoHooksLU):
       that node.
 
     """
-    return rpc.call_export_list(self.op.nodes)
+    return rpc.call_export_list([node.name for node in self.nodes])
 
 
 class LUExportInstance(LogicalUnit):
diff --git a/lib/mcpu.py b/lib/mcpu.py
index f672040..daf7923 100644
--- a/lib/mcpu.py
+++ b/lib/mcpu.py
@@ -59,6 +59,7 @@ class Processor(object):
     opcodes.OpAddNode: cmdlib.LUAddNode,
     opcodes.OpQueryNodes: cmdlib.LUQueryNodes,
     opcodes.OpQueryNodeData: cmdlib.LUQueryNodeData,
+    opcodes.OpQueryNodeVolumes: cmdlib.LUQueryNodeVolumes,
     opcodes.OpRemoveNode: cmdlib.LURemoveNode,
     # instance lu
     opcodes.OpCreateInstance: cmdlib.LUCreateInstance,
diff --git a/lib/opcodes.py b/lib/opcodes.py
index 45bbf1e..a54bb43 100644
--- a/lib/opcodes.py
+++ b/lib/opcodes.py
@@ -122,6 +122,12 @@ class OpQueryNodeData(OpCode):
   __slots__ = ["nodes"]
 
 
+class OpQueryNodeVolumes(OpCode):
+  """Get list of volumes on node."""
+  OP_ID = "OP_NODE_QUERYVOLS"
+  __slots__ = ["nodes", "output_fields"]
+
+
 # instance opcodes
 
 class OpCreateInstance(OpCode):
diff --git a/lib/rpc.py b/lib/rpc.py
index ddb10ae..ce4d787 100644
--- a/lib/rpc.py
+++ b/lib/rpc.py
@@ -762,3 +762,15 @@ def call_node_leave_cluster(node):
   c.connect(node)
   c.run()
   return c.getresult().get(node, False)
+
+
+def call_node_volumes(node_list):
+  """Gets all volumes on node(s).
+
+  This is a multi-node call.
+
+  """
+  c = Client("node_volumes", [])
+  c.connect_list(node_list)
+  c.run()
+  return c.getresult()
diff --git a/man/gnt-node.sgml b/man/gnt-node.sgml
index f0d0119..7341f13 100644
--- a/man/gnt-node.sgml
+++ b/man/gnt-node.sgml
@@ -261,6 +261,30 @@
       </para>
     </refsect2>
 
+    <refsect2>
+      <title>VOLUMES</title>
+
+      <cmdsynopsis>
+        <command>volumes</command>
+        <arg rep="repeat"><replaceable>node</replaceable></arg>
+      </cmdsynopsis>
+
+      <para>
+        Lists all logical volumes and their physical disks from the node(s)
+        provided.
+      </para>
+
+      <para>
+        Example:
+        <screen>
+# gnt-node volumes node5.example.com
+Node              PhysDev   VG    Name                                 Size Instance
+node1.example.com /dev/hdc1 xenvg instance1.example.com-sda_11000.meta 128  instance1.example.com
+node1.example.com /dev/hdc1 xenvg instance1.example.com-sda_11001.data 256  instance1.example.com
+        </screen>
+      </para>
+    </refsect2>
+
   </refsect1>
 
   &footer;
diff --git a/scripts/gnt-instance b/scripts/gnt-instance
index a9fc2f8..45f6795 100755
--- a/scripts/gnt-instance
+++ b/scripts/gnt-instance
@@ -489,11 +489,7 @@ commands = {
   'info': (ShowInstanceConfig, ARGS_ANY, [DEBUG_OPT], "[<instance>...]",
            "Show information on the specified instance"),
   'list': (ListInstances, ARGS_NONE,
-           [DEBUG_OPT, NOHDR_OPT, SEP_OPT, USEUNITS_OPT,
-            make_option("-o", "--output", dest="output", action="store",
-                        type="string", help="Select output fields",
-                        metavar="FIELDS")
-            ],
+           [DEBUG_OPT, NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT],
            "", "Lists the instances and their status"),
   'remove': (RemoveInstance, ARGS_ONE, [DEBUG_OPT, force_opt],
              "[-f] <instance>", "Shuts down the instance and removes it"),
diff --git a/scripts/gnt-node b/scripts/gnt-node
index 88e8c29..7593f89 100755
--- a/scripts/gnt-node
+++ b/scripts/gnt-node
@@ -130,6 +130,66 @@ def RemoveNode(opts, args):
   SubmitOpCode(op)
 
 
+def ListVolumes(opts, args):
+  """List logical volumes on node(s).
+
+  """
+  if opts.output is None:
+    selected_fields = ["node", "phys", "vg",
+                       "name", "size", "instance"]
+  else:
+    selected_fields = opts.output.split(",")
+
+  op = opcodes.OpQueryNodeVolumes(nodes=args, output_fields=selected_fields)
+  output = SubmitOpCode(op)
+
+  mlens = [0 for name in selected_fields]
+  format_fields = []
+  unitformat_fields = ("size",)
+  for field in selected_fields:
+    if field in unitformat_fields:
+      format_fields.append("%*s")
+    else:
+      format_fields.append("%-*s")
+
+  separator = opts.separator
+  if "%" in separator:
+    separator = separator.replace("%", "%%")
+  format = separator.join(format_fields)
+
+  for row in output:
+    for idx, val in enumerate(row):
+      if opts.human_readable and selected_fields[idx] in unitformat_fields:
+        try:
+          val = int(val)
+        except ValueError:
+          pass
+        else:
+          val = row[idx] = utils.FormatUnit(val)
+      mlens[idx] = max(mlens[idx], len(val))
+
+  if not opts.no_headers:
+    header_list = {"node": "Node", "phys": "PhysDev",
+                   "vg": "VG", "name": "Name",
+                   "size": "Size", "instance": "Instance"}
+    args = []
+    for idx, name in enumerate(selected_fields):
+      hdr = header_list[name]
+      mlens[idx] = max(mlens[idx], len(hdr))
+      args.append(mlens[idx])
+      args.append(hdr)
+    logger.ToStdout(format % tuple(args))
+
+  for row in output:
+    args = []
+    for idx, val in enumerate(row):
+      args.append(mlens[idx])
+      args.append(val)
+    logger.ToStdout(format % tuple(args))
+
+  return 0
+
+
 commands = {
   'add': (AddNode, ARGS_ONE,
           [DEBUG_OPT,
@@ -140,14 +200,13 @@ commands = {
   'info': (ShowNodeConfig, ARGS_ANY, [DEBUG_OPT],
            "[<node_name>...]", "Show information about the node(s)"),
   'list': (ListNodes, ARGS_NONE,
-           [DEBUG_OPT, NOHDR_OPT, SEP_OPT, USEUNITS_OPT,
-            make_option("-o", "--output", dest="output", action="store",
-                        type="string", help="Select output fields",
-                        metavar="FIELDS")
-            ],
+           [DEBUG_OPT, NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT],
            "", "Lists the nodes in the cluster"),
   'remove': (RemoveNode, ARGS_ONE, [DEBUG_OPT],
              "<node_name>", "Removes a node from the cluster"),
+  'volumes': (ListVolumes, ARGS_ANY,
+              [DEBUG_OPT, NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT],
+              "[<node_name>...]", "List logical volumes on node(s)"),
   }
 
 
