From 2bbf77cc91262de0166e7439a97a38472037de61 Mon Sep 17 00:00:00 2001
From: Iustin Pop <iustin@google.com>
Date: Wed, 8 Jul 2009 16:34:46 +0200
Subject: [PATCH] hspace: switch output to shell-script format
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

This (big) patch changes the output of hspace from text-format
(separated by β€˜: ’) to a shell-snippet, in β€˜key=value’ format.

This will allow sourcing the output or parsing it via awk/sed/etc.
---
 Ganeti/HTools/Types.hs |   2 +-
 hspace.1               | 169 ++++++++++++++++++++++++++++-------------
 hspace.hs              | 145 +++++++++++++++++++++--------------
 3 files changed, 206 insertions(+), 110 deletions(-)

diff --git a/Ganeti/HTools/Types.hs b/Ganeti/HTools/Types.hs
index f7f55f593..cb384e852 100644
--- a/Ganeti/HTools/Types.hs
+++ b/Ganeti/HTools/Types.hs
@@ -65,7 +65,7 @@ data FailMode = FailMem  -- ^ Failed due to not enough RAM
               | FailDisk -- ^ Failed due to not enough disk
               | FailCPU  -- ^ Failed due to not enough CPU capacity
               | FailN1   -- ^ Failed due to not passing N1 checks
-                deriving (Eq, Show)
+                deriving (Eq, Enum, Bounded, Show)
 
 -- | Either-like data-type customized for our failure modes
 data OpResult a = OpFail FailMode -- ^ Failed operation
diff --git a/hspace.1 b/hspace.1
index e496423f6..db4235821 100644
--- a/hspace.1
+++ b/hspace.1
@@ -28,8 +28,121 @@ cluster, until the point where we don't have any N+1 possible
 allocation. It uses the exact same allocation algorithm as the hail
 iallocator plugin.
 
-With default options, the output of the program is designed to be
-parseable; when the -p option is passed, this is no longer true.
+The output of the program is designed to interpreted as a shell
+fragment (or parsed as a \fIkey=value\fR file). Options which extend
+the output (e.g. -p, -v) will output the additional information on
+stderr (such that the stdout is still parseable).
+
+The following keys are available in the output of the script (all
+prefixed with \fIHTS_\fR):
+.TP
+.I SPEC_MEM, SPEC_DSK, SPEC_CPU, SPEC_RQN
+These represent the specifications of the instance model used for
+allocation (the memory, disk, cpu, requested nodes).
+
+.TP
+.I CLUSTER_MEM, CLUSTER_DSK, CLUSTER_CPU, CLUSTER_NODES
+These represent the total memory, disk, CPU count and total nodes in
+the cluster.
+
+.TP
+.I INI_SCORE, FIN_SCORE
+These are the initial (current) and final cluster score (see the hbal
+man page for details about the scoring algorithm).
+
+.TP
+.I INI_INST_CNT, FIN_INST_CNT
+The initial and final instance count.
+
+.TP
+.I INI_MEM_FREE, FIN_MEM_FREE
+The initial and final total free memory in the cluster (but this
+doesn't necessarily mean available for use).
+
+.TP
+.I INI_MEM_AVAIL, FIN_MEM_AVAIL
+The initial and final total available memory for allocation in the
+cluster. If allocating redundant instances, new instances could
+increase the reserved memory so it doesn't necessarily mean the
+entirety of this memory can be used for new instance allocations.
+
+.TP
+.I INI_MEM_RESVD, FIN_MEM_RESVD
+The initial and final reserved memory (for redundancy/N+1 purposes).
+
+.TP
+.I INI_MEM_INST, FIN_MEM_INST
+The initial and final memory used for instances (actual runtime used
+RAM).
+
+.TP
+.I INI_MEM_OVERHEAD, FIN_MEM_OVERHEAD
+The initial and final memory overhead - memory used for the node
+itself and unacounted memory (e.g. due to hypervisor overhead).
+
+.TP
+.I INI_MEM_EFF, HTS_INI_MEM_EFF
+The initial and final memory efficiency, represented as instance
+memory divided by total memory.
+
+.TP
+.I INI_DSK_FREE, INI_DSK_AVAIL, INI_DSK_RESVD, INI_DSK_INST, INI_DSK_EFF
+Initial disk stats, similar to the memory ones.
+
+.TP
+.I FIN_DSK_FREE, FIN_DSK_AVAIL, FIN_DSK_RESVD, FIN_DSK_INST, FIN_DSK_EFF
+Final disk stats, similar to the memory ones.
+
+.TP
+.I INI_CPU_INST, FIN_CPU_INST
+Initial and final number of virtual CPUs used by instances.
+
+.TP
+.I INI_CPU_EFF, FIN_CPU_EFF
+The initial and final CPU efficiency, represented as the count of
+virtual instance CPUs divided by the total physical CPU count.
+
+.TP
+.I INI_MNODE_MEM_AVAIL, FIN_MNODE_MEM_AVAIL
+The initial and final maximum per-node available memory. This is not
+very useful as a metric but can give an impression of the status of
+the nodes; as an example, this value restricts the maximum instance
+size that can be still created on the cluster.
+
+.TP
+.I INI_MNODE_DSK_AVAIL, FIN_MNODE_DSK_AVAIL
+Like the above but for disk.
+
+.TP
+.I ALLOC_USAGE
+The current usage represented as initial number of instances divided
+per final number of instances.
+
+.TP
+.I ALLOC_COUNT
+The number of instances allocated (delta between FIN_INST_CNT and
+INI_INST_CNT).
+
+.TP
+.I ALLOC_FAIL*_CNT
+For the last attemp at allocations (which would have increased
+FIN_INST_CNT with one, if it had succeeded), this is the count of the
+failure reasons per failure type; currently defined are FAILMEM,
+FAILDISK and FAILCPU which represent errors due to not enough memory,
+disk and CPUs, and FAILN1 which represents a non N+1 compliant cluster
+on which we can't allocate instances at all.
+
+.TP
+.I ALLOC_FAIL_REASON
+The reason for most of the failures, being one of the above FAIL*
+strings.
+
+.TP
+.I OK
+A marker representing the successful end of the computation, and
+having value "1". If this key is not present in the output it means
+that the computation failed and any values present should not be
+relied upon.
 
 .SH OPTIONS
 The options that can be passed to the program are as follows:
@@ -162,7 +275,7 @@ considered a fully-specified URL and is used as-is.
 Increase the output verbosity. Each usage of this option will increase
 the verbosity (currently more than 2 doesn't make sense) from the
 default of one. At verbosity 2 the location of the new instances is
-shown in program output.
+shown in the standard error.
 
 .TP
 .B -q, --quiet
@@ -189,56 +302,6 @@ The algorithm doesn't rebalance the cluster or try to get the optimal
 fit; it just allocates in the best place for the current step, without
 taking into consideration the impact on future placements.
 
-.SH EXAMPLE
-
-.SS Default output
-
-.in +4n
-.nf
-.RB "$" " hspace --mem 16 --disk 16 --req-nodes 2"
-Initial score: 0.38988095
-Initial instances: 3
-Initial free RAM: 546
-Initial free disk: 260600
-Final score: 0.32638889
-Final instances: 7
-Final free RAM: 482
-Final free disk: 260472
-Usage: 0.43
-Allocations: 4
-.fi
-.in
-
-This shows that (on this fake cluster), starting from 3 initial
-instances, using the hail iallocator plugin, it would be possible to
-add four (Allocations: 4) new instances to the cluster.
-
-.SS Verbose output
-
-For the same cluster as above:
-.in +4n
-.nf
-.RB "$" " hspace --mem 16 --disk 16 --req-nodes 2 -v"
-Initial score: 0.38988095
-Initial instances: 3
-Initial free RAM: 546
-Initial free disk: 260600
-Final score: 0.32638889
-Final instances: 7
-Final free RAM: 482
-Final free disk: 260472
-Usage: 0.43
-Allocations: 4
-Inst: new-0 node2 node1
-Inst: new-1 node2 node1
-Inst: new-2 node2 node1
-Inst: new-3 node2 node1
-.fi
-.in
-
-The output now includes the placement for the new instances (named
-\fBnew-\fInumber\fR).
-
 .SH ENVIRONMENT
 
 If the variables \fBHTOOLS_NODES\fR and \fBHTOOLS_INSTANCES\fR are
diff --git a/hspace.hs b/hspace.hs
index 4b1417894..8159f4418 100644
--- a/hspace.hs
+++ b/hspace.hs
@@ -25,6 +25,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 
 module Main (main) where
 
+import Data.Char (toUpper)
 import Data.List
 import Data.Function
 import Monad
@@ -147,6 +148,50 @@ options =
       "show help"
     ]
 
+data Phase = PInitial | PFinal
+
+statsData :: [(String, Cluster.CStats -> String)]
+statsData = [ ("SCORE", printf "%.8f" . Cluster.cs_score)
+            , ("INST_CNT", printf "%d" . Cluster.cs_ninst)
+            , ("MEM_FREE", printf "%d" . Cluster.cs_fmem)
+            , ("MEM_AVAIL", printf "%d" . Cluster.cs_amem)
+            , ("MEM_RESVD",
+               \cs -> printf "%d" (Cluster.cs_fmem cs - Cluster.cs_amem cs))
+            , ("MEM_INST", printf "%d" . Cluster.cs_imem)
+            , ("MEM_OVERHEAD",
+               \cs -> printf "%d" (Cluster.cs_xmem cs + Cluster.cs_nmem cs))
+            , ("MEM_EFF",
+               \cs -> printf "%.8f" (fromIntegral (Cluster.cs_imem cs) /
+                                     Cluster.cs_tmem cs))
+            , ("DSK_FREE", printf "%d" . Cluster.cs_fdsk)
+            , ("DSK_AVAIL", printf "%d ". Cluster.cs_adsk)
+            , ("DSK_RESVD",
+               \cs -> printf "%d" (Cluster.cs_fdsk cs - Cluster.cs_adsk cs))
+            , ("DSK_INST", printf "%d" . Cluster.cs_idsk)
+            , ("DSK_EFF",
+               \cs -> printf "%.8f" (fromIntegral (Cluster.cs_idsk cs) /
+                                    Cluster.cs_tdsk cs))
+            , ("CPU_INST", printf "%d" . Cluster.cs_icpu)
+            , ("CPU_EFF",
+               \cs -> printf "%.8f" (fromIntegral (Cluster.cs_icpu cs) /
+                                     Cluster.cs_tcpu cs))
+            , ("MNODE_MEM_AVAIL", printf "%d" . Cluster.cs_mmem)
+            , ("MNODE_DSK_AVAIL", printf "%d" . Cluster.cs_mdsk)
+            ]
+
+specData :: [(String, Options -> String)]
+specData = [ ("MEM", printf "%d" . optIMem)
+           , ("DSK", printf "%d" . optIDsk)
+           , ("CPU", printf "%d" . optIVCPUs)
+           , ("RQN", printf "%d" . optINodes)
+           ]
+
+clusterData :: [(String, Cluster.CStats -> String)]
+clusterData = [ ("MEM", printf "%.0f" . Cluster.cs_tmem)
+              , ("DSK", printf "%.0f" . Cluster.cs_tdsk)
+              , ("CPU", printf "%.0f" . Cluster.cs_tcpu)
+              ]
+
 -- | Build failure stats out of a list of failure reasons
 concatFailure :: [(FailMode, Int)] -> FailMode -> [(FailMode, Int)]
 concatFailure flst reason =
@@ -169,7 +214,7 @@ filterFails sols =
                                    ) $ sols
         aval = concat alst
         bval = concat blst
-    in (foldl' concatFailure [] aval, bval)
+    in (foldl' concatFailure [(x, 0) | x <- [minBound..maxBound]] aval, bval)
 
 -- | Get the placement with best score out of a list of possible placements
 processResults :: [(Node.List, Instance.Instance, [Node.Node])]
@@ -203,30 +248,12 @@ iterateDepth nl il newinst nreq ixes =
                        in iterateDepth xnl il newinst nreq (xi:ixes)
 
 -- | Function to print stats for a given phase
-printStats :: String -> Cluster.CStats -> IO ()
-printStats kind cs = do
-  printf "%s score: %.8f\n" kind (Cluster.cs_score cs)
-  printf "%s instances: %d\n" kind (Cluster.cs_ninst cs)
-  printf "%s free RAM: %d\n" kind (Cluster.cs_fmem cs)
-  printf "%s allocatable RAM: %d\n" kind (Cluster.cs_amem cs)
-  printf "%s reserved RAM: %d\n" kind (Cluster.cs_fmem cs -
-                                       Cluster.cs_amem cs)
-  printf "%s instance RAM: %d\n" kind (Cluster.cs_imem cs)
-  printf "%s overhead RAM: %d\n" kind (Cluster.cs_xmem cs + Cluster.cs_nmem cs)
-  printf "%s RAM usage efficiency: %.8f\n"
-         kind (fromIntegral (Cluster.cs_imem cs) / Cluster.cs_tmem cs)
-  printf "%s free disk: %d\n" kind (Cluster.cs_fdsk cs)
-  printf "%s allocatable disk: %d\n" kind (Cluster.cs_adsk cs)
-  printf "%s reserved disk: %d\n" kind (Cluster.cs_fdsk cs -
-                                        Cluster.cs_adsk cs)
-  printf "%s instance disk: %d\n" kind (Cluster.cs_idsk cs)
-  printf "%s disk usage efficiency: %.8f\n"
-         kind (fromIntegral (Cluster.cs_idsk cs) / Cluster.cs_tdsk cs)
-  printf "%s instance cpus: %d\n" kind (Cluster.cs_icpu cs)
-  printf "%s cpu usage efficiency: %.8f\n"
-         kind (fromIntegral (Cluster.cs_icpu cs) / Cluster.cs_tcpu cs)
-  printf "%s max node allocatable RAM: %d\n" kind (Cluster.cs_mmem cs)
-  printf "%s max node allocatable disk: %d\n" kind (Cluster.cs_mdsk cs)
+printStats :: Phase -> Cluster.CStats -> [(String, String)]
+printStats ph cs =
+  map (\(s, fn) -> (printf "%s_%s" kind s, fn cs)) statsData
+  where kind = case ph of
+                 PInitial -> "INI"
+                 PFinal -> "FIN"
 
 -- | Print final stats and related metrics
 printResults :: Node.List -> Int -> Int -> [(FailMode, Int)] -> IO ()
@@ -241,12 +268,21 @@ printResults fin_nl num_instances allocs sreason = do
                                  (Cluster.cs_ninst fin_stats)
          exitWith $ ExitFailure 1
 
-  printStats "Final" fin_stats
-  printf "Usage: %.5f\n" ((fromIntegral num_instances::Double) /
-                          fromIntegral fin_instances)
-  printf "Allocations: %d\n" allocs
-  putStr (unlines . map (\(x, y) -> printf "%s: %d" (show x) y) $ sreason)
-  printf "Most likely fail reason: %s\n" (show . fst . head $ sreason)
+  printKeys $ printStats PFinal fin_stats
+  printKeys [ ("ALLOC_USAGE", printf "%.8f"
+                                ((fromIntegral num_instances::Double) /
+                                 fromIntegral fin_instances))
+            , ("ALLOC_COUNT", printf "%d" allocs)
+            , ("ALLOC_FAIL_REASON", map toUpper . show . fst $ head sreason)
+            ]
+  printKeys $ map (\(x, y) -> (printf "ALLOC_%s_CNT" (show x),
+                               printf "%d" y)) sreason
+  -- this should be the final entry
+  printKeys [("OK", "1")]
+
+-- | Format a list of key/values as a shell fragment
+printKeys :: [(String, String)] -> IO ()
+printKeys = mapM_ (\(k, v) -> printf "HTS_%s=%s\n" (map toUpper k) v)
 
 -- | Main function.
 main :: IO ()
@@ -262,10 +298,7 @@ main = do
 
   (fixed_nl, il, csf) <- CLI.loadExternalData opts
 
-  printf "Spec RAM: %d\n" (optIMem opts)
-  printf "Spec disk: %d\n" (optIDsk opts)
-  printf "Spec CPUs: %d\n" (optIVCPUs opts)
-  printf "Spec nodes: %d\n" (optINodes opts)
+  printKeys $ map (\(a, fn) -> ("SPEC_" ++ a, fn opts)) specData
 
   let num_instances = length $ Container.elems il
 
@@ -296,24 +329,23 @@ main = do
            nm
 
   when (length csf > 0 && verbose > 1) $
-       printf "Note: Stripping common suffix of '%s' from names\n" csf
+       hPrintf stderr "Note: Stripping common suffix of '%s' from names\n" csf
 
   when (optShowNodes opts) $
        do
-         putStrLn "Initial cluster status:"
-         putStrLn $ Cluster.printNodes nl
+         hPutStrLn stderr "Initial cluster status:"
+         hPutStrLn stderr $ Cluster.printNodes nl
 
   let ini_cv = Cluster.compCV nl
       ini_stats = Cluster.totalResources nl
 
   when (verbose > 2) $ do
-         printf "Initial coefficients: overall %.8f, %s\n"
-                ini_cv (Cluster.printStats nl)
+         hPrintf stderr "Initial coefficients: overall %.8f, %s\n"
+                 ini_cv (Cluster.printStats nl)
 
-  printf "Cluster RAM: %.0f\n" (Cluster.cs_tmem ini_stats)
-  printf "Cluster disk: %.0f\n" (Cluster.cs_tdsk ini_stats)
-  printf "Cluster cpus: %.0f\n" (Cluster.cs_tcpu ini_stats)
-  printStats "Initial" ini_stats
+  printKeys $ map (\(a, fn) -> ("CLUSTER_" ++ a, fn ini_stats)) clusterData
+  printKeys [("CLUSTER_NODES", printf "%d" (length all_nodes))]
+  printKeys $ printStats PInitial ini_stats
 
   let bad_nodes = fst $ Cluster.computeBadItems nl il
   when (length bad_nodes > 0) $ do
@@ -332,19 +364,20 @@ main = do
       ix_namelen = maximum . map (length . Instance.name) $ fin_ixes
       sreason = reverse $ sortBy (compare `on` snd) ereason
 
-  printResults fin_nl num_instances allocs sreason
-
   when (verbose > 1) $
-         putStr . unlines . map (\i -> printf "Inst: %*s %-*s %-*s"
-                     ix_namelen (Instance.name i)
-                     nmlen (Container.nameOf fin_nl $ Instance.pnode i)
-                     nmlen (let sdx = Instance.snode i
-                            in if sdx == Node.noSecondary then ""
-                               else Container.nameOf fin_nl sdx))
-         $ fin_ixes
+         hPutStr stderr . unlines $
+         map (\i -> printf "Inst: %*s %-*s %-*s"
+                    ix_namelen (Instance.name i)
+                    nmlen (Container.nameOf fin_nl $ Instance.pnode i)
+                    nmlen (let sdx = Instance.snode i
+                           in if sdx == Node.noSecondary then ""
+                              else Container.nameOf fin_nl sdx)
+             ) fin_ixes
 
   when (optShowNodes opts) $
        do
-         putStrLn ""
-         putStrLn "Final cluster status:"
-         putStrLn $ Cluster.printNodes fin_nl
+         hPutStrLn stderr ""
+         hPutStrLn stderr "Final cluster status:"
+         hPutStrLn stderr $ Cluster.printNodes fin_nl
+
+  printResults fin_nl num_instances allocs sreason
-- 
GitLab