Change the tryAlloc/tryReloc workflow
[ganeti-local] / hspace.hs
index 4a5c2f1..ce09d5c 100644 (file)
--- a/hspace.hs
+++ b/hspace.hs
@@ -25,16 +25,16 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 
 module Main (main) where
 
+import Data.Char (toUpper)
 import Data.List
 import Data.Function
-import Data.Maybe (isJust, fromJust, isNothing)
 import Monad
 import System
 import System.IO
 import System.Console.GetOpt
 import qualified System
 
-import Text.Printf (printf)
+import Text.Printf (printf, hPrintf)
 
 import qualified Ganeti.HTools.Container as Container
 import qualified Ganeti.HTools.Cluster as Cluster
@@ -43,6 +43,7 @@ import qualified Ganeti.HTools.Instance as Instance
 import qualified Ganeti.HTools.CLI as CLI
 
 import Ganeti.HTools.Utils
+import Ganeti.HTools.Types
 
 -- | Command line options structure.
 data Options = Options
@@ -74,7 +75,7 @@ instance CLI.EToolOptions Options where
     instFile   = optInstf
     instSet    = optInstSet
     masterName = optMaster
-    silent a   = (optVerbose a) == 0
+    silent a   = optVerbose a == 0
 
 -- | Default values for the command line options.
 defaultOptions :: Options
@@ -113,10 +114,10 @@ options =
       (ReqArg (\ m opts -> opts { optMaster = m }) "ADDRESS")
       "collect data via RAPI at the given ADDRESS"
     , Option ['v']     ["verbose"]
-      (NoArg (\ opts -> opts { optVerbose = (optVerbose opts) + 1 }))
+      (NoArg (\ opts -> opts { optVerbose = optVerbose opts + 1 }))
       "increase the verbosity level"
     , Option ['q']     ["quiet"]
-      (NoArg (\ opts -> opts { optVerbose = (optVerbose opts) - 1 }))
+      (NoArg (\ opts -> opts { optVerbose = optVerbose opts - 1 }))
       "decrease the verbosity level"
     , Option ['O']     ["offline"]
       (ReqArg (\ n opts -> opts { optOffline = n:optOffline opts }) "NODE")
@@ -147,58 +148,108 @@ options =
       "show help"
     ]
 
-filterFails :: Cluster.AllocSolution
-            -> Maybe [(Node.List, Instance.Instance, [Node.Node])]
-filterFails sols =
-    if null sols then Nothing -- No nodes onto which to allocate at all
-    else let sols' = filter (isJust . fst3) sols
-         in if null sols' then
-                Nothing -- No valid allocation solutions
-            else
-                return $ map (\(x, y, z) -> (fromJust x, y, z)) sols'
-
-processResults :: (Monad m) => [(Node.List, Instance.Instance, [Node.Node])]
-               -> m (Node.List, Instance.Instance, [Node.Node])
-processResults sols =
-    let sols' = map (\e@(nl', _, _) -> (Cluster.compCV  nl', e)) sols
-        sols'' = sortBy (compare `on` fst) sols'
-    in return $ snd $ head sols''
-
+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)
+              ]
+
+-- | Recursively place instances on the cluster until we're out of space
 iterateDepth :: Node.List
              -> Instance.List
              -> Instance.Instance
              -> Int
              -> [Instance.Instance]
-             -> (Node.List, [Instance.Instance])
+             -> ([(FailMode, Int)], Node.List, [Instance.Instance])
 iterateDepth nl il newinst nreq ixes =
       let depth = length ixes
-          newname = (printf "new-%d" depth)::String
-          newidx = (length $ Container.elems il) + depth
+          newname = printf "new-%d" depth::String
+          newidx = length (Container.elems il) + depth
           newi2 = Instance.setIdx (Instance.setName newinst newname) newidx
-          sols = (Cluster.tryAlloc nl il newi2 nreq)::
-                 Maybe Cluster.AllocSolution
-          orig = (nl, ixes)
-      in
-        if isNothing sols then orig
-        else let sols' = fromJust sols
-                 sols'' = filterFails sols'
-             in if isNothing sols'' then orig
-                else let (xnl, xi, _) = fromJust $ processResults $
-                                        fromJust sols''
-                     in iterateDepth xnl il newinst nreq (xi:ixes)
-
-printStats :: String -> Cluster.CStats -> IO ()
-printStats kind cs = do
-  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 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 max node allocatable RAM: %d\n" kind (Cluster.cs_mmem cs)
-  printf "%s max node allocatable disk: %d\n" kind (Cluster.cs_mdsk cs)
+          sols = Cluster.tryAlloc nl il newi2 nreq::
+                 OpResult Cluster.AllocSolution
+      in case sols of
+           OpFail _ -> ([], nl, ixes)
+           OpGood (errs, _, sols3) ->
+               case sols3 of
+                 Nothing -> (Cluster.collapseFailures errs, nl, ixes)
+                 Just (_, (xnl, xi, _)) ->
+                     iterateDepth xnl il newinst nreq $! (xi:ixes)
+
+-- | Function to print stats for a given phase
+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 ()
+printResults fin_nl num_instances allocs sreason = do
+  let fin_stats = Cluster.totalResources fin_nl
+      fin_instances = num_instances + allocs
+
+  when (num_instances + allocs /= Cluster.cs_ninst fin_stats) $
+       do
+         hPrintf stderr "ERROR: internal inconsistency, allocated (%d)\
+                        \ != counted (%d)\n" (num_instances + allocs)
+                                 (Cluster.cs_ninst fin_stats)
+         exitWith $ ExitFailure 1
+
+  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 ()
@@ -213,12 +264,15 @@ main = do
   let verbose = optVerbose opts
 
   (fixed_nl, il, csf) <- CLI.loadExternalData opts
+
+  printKeys $ map (\(a, fn) -> ("SPEC_" ++ a, fn opts)) specData
+
   let num_instances = length $ Container.elems il
 
   let offline_names = optOffline opts
       all_nodes = Container.elems fixed_nl
       all_names = map Node.name all_nodes
-      offline_wrong = filter (\n -> not $ elem n all_names) offline_names
+      offline_wrong = filter (flip notElem all_names) offline_names
       offline_indices = map Node.idx $
                         filter (\n -> elem (Node.name n) offline_names)
                                all_nodes
@@ -227,12 +281,12 @@ main = do
       m_dsk = optMdsk opts
 
   when (length offline_wrong > 0) $ do
-         printf "Error: Wrong node name(s) set as offline: %s\n"
-                (commaJoin offline_wrong)
+         hPrintf stderr "Error: Wrong node name(s) set as offline: %s\n"
+                     (commaJoin offline_wrong)
          exitWith $ ExitFailure 1
 
   when (req_nodes /= 1 && req_nodes /= 2) $ do
-         printf "Error: Invalid required nodes (%d)\n" req_nodes
+         hPrintf stderr "Error: Invalid required nodes (%d)\n" req_nodes
          exitWith $ ExitFailure 1
 
   let nm = Container.map (\n -> if elem (Node.idx n) offline_indices
@@ -241,58 +295,56 @@ main = do
       nl = Container.map (flip Node.setMdsk m_dsk . flip Node.setMcpu m_cpu)
            nm
 
-  when (length csf > 0 && verbose > 1) $ do
-         printf "Note: Stripping common suffix of '%s' from names\n" csf
-
-  let bad_nodes = fst $ Cluster.computeBadItems nl il
-  when (length bad_nodes > 0) $ do
-         putStrLn "Error: Cluster not N+1, no space to allocate."
-         exitWith $ ExitFailure 1
+  when (length csf > 0 && verbose > 1) $
+       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
 
-  (if verbose > 2 then
-       printf "Initial coefficients: overall %.8f, %s\n"
-       ini_cv (Cluster.printStats nl)
-   else
-       printf "Initial score: %.8f\n" ini_cv)
-  printf "Initial instances: %d\n" num_instances
-  printStats "Initial" ini_stats
+  when (verbose > 2) $ do
+         hPrintf stderr "Initial coefficients: overall %.8f, %s\n"
+                 ini_cv (Cluster.printStats nl)
+
+  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
+         -- This is failn1 case, so we print the same final stats and
+         -- exit early
+         printResults nl num_instances 0 [(FailN1, 1)]
+         exitWith ExitSuccess
 
   let nmlen = Container.maxNameLen nl
       newinst = Instance.create "new" (optIMem opts) (optIDsk opts)
                 (optIVCPUs opts) "ADMIN_down" (-1) (-1)
 
-  let (fin_nl, ixes) = iterateDepth nl il newinst req_nodes []
+  let (ereason, fin_nl, ixes) = iterateDepth nl il newinst req_nodes []
       allocs = length ixes
-      fin_instances = num_instances + allocs
       fin_ixes = reverse ixes
       ix_namelen = maximum . map (length . Instance.name) $ fin_ixes
-      fin_stats = Cluster.totalResources fin_nl
-
-  printf "Final score: %.8f\n" (Cluster.compCV fin_nl)
-  printf "Final instances: %d\n" (num_instances + allocs)
-  printStats "Final" fin_stats
-  printf "Usage: %.5f\n" (((fromIntegral num_instances)::Double) /
-                          (fromIntegral fin_instances))
-  printf "Allocations: %d\n" allocs
-  when (verbose > 1) $ do
-         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
+      sreason = reverse $ sortBy (compare `on` snd) ereason
+
+  when (verbose > 1) $
+         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