Fix prefix bug in Haskell ssconf implementation
[ganeti-local] / htools / Ganeti / HTools / Program / Hbal.hs
index ca6b9d7..c6cb629 100644 (file)
@@ -4,7 +4,7 @@
 
 {-
 
-Copyright (C) 2009, 2010, 2011 Google Inc.
+Copyright (C) 2009, 2010, 2011, 2012 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
@@ -23,7 +23,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 
 -}
 
-module Ganeti.HTools.Program.Hbal (main) where
+module Ganeti.HTools.Program.Hbal (main, options) where
 
 import Control.Concurrent (threadDelay)
 import Control.Exception (bracket)
@@ -31,11 +31,10 @@ import Control.Monad
 import Data.List
 import Data.Maybe (isJust, isNothing, fromJust)
 import Data.IORef
-import System (exitWith, ExitCode(..))
+import System.Exit
 import System.IO
 import System.Posix.Process
 import System.Posix.Signals
-import qualified System
 
 import Text.Printf (printf, hPrintf)
 
@@ -54,38 +53,38 @@ import Ganeti.HTools.Loader
 import qualified Ganeti.Luxi as L
 import Ganeti.Jobs
 
--- | Options list and functions
+-- | Options list and functions.
 options :: [OptType]
 options =
-    [ oPrintNodes
-    , oPrintInsts
-    , oPrintCommands
-    , oOneline
-    , oDataFile
-    , oEvacMode
-    , oRapiMaster
-    , oLuxiSocket
-    , oExecJobs
-    , oGroup
-    , oMaxSolLength
-    , oVerbose
-    , oQuiet
-    , oOfflineNode
-    , oMinScore
-    , oMaxCpu
-    , oMinDisk
-    , oMinGain
-    , oMinGainLim
-    , oDiskMoves
-    , oSelInst
-    , oInstMoves
-    , oDynuFile
-    , oExTags
-    , oExInst
-    , oSaveCluster
-    , oShowVer
-    , oShowHelp
-    ]
+  [ oPrintNodes
+  , oPrintInsts
+  , oPrintCommands
+  , oDataFile
+  , oEvacMode
+  , oRapiMaster
+  , oLuxiSocket
+  , oIAllocSrc
+  , oExecJobs
+  , oGroup
+  , oMaxSolLength
+  , oVerbose
+  , oQuiet
+  , oOfflineNode
+  , oMinScore
+  , oMaxCpu
+  , oMinDisk
+  , oMinGain
+  , oMinGainLim
+  , oDiskMoves
+  , oSelInst
+  , oInstMoves
+  , oDynuFile
+  , oExTags
+  , oExInst
+  , oSaveCluster
+  , oShowVer
+  , oShowHelp
+  ]
 
 {- | Start computing the solution at the given depth and recurse until
 we find a valid solution or we exceed the maximum depth.
@@ -98,7 +97,6 @@ iterateDepth :: Cluster.Table    -- ^ The starting table
              -> Int              -- ^ Max node name len
              -> Int              -- ^ Max instance name len
              -> [MoveJob]        -- ^ Current command list
-             -> Bool             -- ^ Whether to be silent
              -> Score            -- ^ Score at which to stop
              -> Score            -- ^ Min gain limit
              -> Score            -- ^ Min score gain
@@ -106,41 +104,56 @@ iterateDepth :: Cluster.Table    -- ^ The starting table
              -> IO (Cluster.Table, [MoveJob]) -- ^ The resulting table
                                               -- and commands
 iterateDepth ini_tbl max_rounds disk_moves inst_moves nmlen imlen
-             cmd_strs oneline min_score mg_limit min_gain evac_mode =
-    let Cluster.Table ini_nl ini_il _ _ = ini_tbl
-        allowed_next = Cluster.doNextBalance ini_tbl max_rounds min_score
-        m_fin_tbl = if allowed_next
+             cmd_strs min_score mg_limit min_gain evac_mode =
+  let Cluster.Table ini_nl ini_il _ _ = ini_tbl
+      allowed_next = Cluster.doNextBalance ini_tbl max_rounds min_score
+      m_fin_tbl = if allowed_next
                     then Cluster.tryBalance ini_tbl disk_moves inst_moves
                          evac_mode mg_limit min_gain
                     else Nothing
-    in
-      case m_fin_tbl of
-        Just fin_tbl ->
-            do
-              let
-                  (Cluster.Table _ _ _ fin_plc) = fin_tbl
-                  fin_plc_len = length fin_plc
-                  cur_plc@(idx, _, _, move, _) = head fin_plc
-                  (sol_line, cmds) = Cluster.printSolutionLine ini_nl ini_il
-                                     nmlen imlen cur_plc fin_plc_len
-                  afn = Cluster.involvedNodes ini_il cur_plc
-                  upd_cmd_strs = (afn, idx, move, cmds):cmd_strs
-              unless oneline $ do
-                       putStrLn sol_line
-                       hFlush stdout
-              iterateDepth fin_tbl max_rounds disk_moves inst_moves
-                           nmlen imlen upd_cmd_strs oneline min_score
-                           mg_limit min_gain evac_mode
-        Nothing -> return (ini_tbl, cmd_strs)
-
--- | Formats the solution for the oneline display
-formatOneline :: Double -> Int -> Double -> String
-formatOneline ini_cv plc_len fin_cv =
-    printf "%.8f %d %.8f %8.3f" ini_cv plc_len fin_cv
-               (if fin_cv == 0 then 1 else ini_cv / fin_cv)
+  in case m_fin_tbl of
+       Just fin_tbl ->
+         do
+           let (Cluster.Table _ _ _ fin_plc) = fin_tbl
+               fin_plc_len = length fin_plc
+               cur_plc@(idx, _, _, move, _) = head fin_plc
+               (sol_line, cmds) = Cluster.printSolutionLine ini_nl ini_il
+                                  nmlen imlen cur_plc fin_plc_len
+               afn = Cluster.involvedNodes ini_il cur_plc
+               upd_cmd_strs = (afn, idx, move, cmds):cmd_strs
+           putStrLn sol_line
+           hFlush stdout
+           iterateDepth fin_tbl max_rounds disk_moves inst_moves
+                        nmlen imlen upd_cmd_strs min_score
+                        mg_limit min_gain evac_mode
+       Nothing -> return (ini_tbl, cmd_strs)
+
+-- | Displays the cluster stats.
+printStats :: Node.List -> Node.List -> IO ()
+printStats ini_nl fin_nl = do
+  let ini_cs = Cluster.totalResources ini_nl
+      fin_cs = Cluster.totalResources fin_nl
+  printf "Original: mem=%d disk=%d\n"
+             (Cluster.csFmem ini_cs) (Cluster.csFdsk ini_cs) :: IO ()
+  printf "Final:    mem=%d disk=%d\n"
+             (Cluster.csFmem fin_cs) (Cluster.csFdsk fin_cs)
+
+-- | Saves the rebalance commands to a text file.
+saveBalanceCommands :: Options -> String -> IO ()
+saveBalanceCommands opts cmd_data = do
+  let out_path = fromJust $ optShowCmds opts
+  putStrLn ""
+  if out_path == "-"
+    then printf "Commands to run to reach the above solution:\n%s"
+           (unlines . map ("  " ++) .
+            filter (/= "  check") .
+            lines $ cmd_data)
+    else do
+      writeFile out_path (shTemplate ++ cmd_data)
+      printf "The commands have been written to file '%s'\n" out_path
 
 -- | Polls a set of jobs at a fixed interval until all are finished
--- one way or another
+-- one way or another.
 waitForJobs :: L.Client -> [String] -> IO (Result [JobStatus])
 waitForJobs client jids = do
   sts <- L.queryJobsStatus client jids
@@ -153,24 +166,24 @@ waitForJobs client jids = do
               waitForJobs client jids
             else return $ Ok s
 
--- | Check that a set of job statuses is all success
+-- | Check that a set of job statuses is all success.
 checkJobsStatus :: [JobStatus] -> Bool
 checkJobsStatus = all (== JOB_STATUS_SUCCESS)
 
--- | Wrapper over execJobSet checking for early termination
+-- | Wrapper over execJobSet checking for early termination.
 execWrapper :: String -> Node.List
-           -> Instance.List -> IORef Int -> [JobSet] -> IO Bool
+            -> Instance.List -> IORef Int -> [JobSet] -> IO Bool
 execWrapper _      _  _  _    [] = return True
 execWrapper master nl il cref alljss = do
   cancel <- readIORef cref
-  (if cancel > 0
-   then do
-     hPrintf stderr "Exiting early due to user request, %d\
-                    \ jobset(s) remaining." (length alljss)::IO ()
-     return False
-   else execJobSet master nl il cref alljss)
-
--- | Execute an entire jobset
+  if cancel > 0
+    then do
+      hPrintf stderr "Exiting early due to user request, %d\
+                     \ jobset(s) remaining." (length alljss)::IO ()
+      return False
+    else execJobSet master nl il cref alljss
+
+-- | Execute an entire jobset.
 execJobSet :: String -> Node.List
            -> Instance.List -> IORef Int -> [JobSet] -> IO Bool
 execJobSet _      _  _  _    [] = return True
@@ -189,26 +202,42 @@ execJobSet master nl il cref (js:jss) = do
                 putStrLn $ "Got job IDs " ++ commaJoin x
                 waitForJobs client x
          )
-  (case jrs of
-     Bad x -> do
-       hPutStrLn stderr $ "Cannot compute job status, aborting: " ++ show x
-       return False
-     Ok x -> if checkJobsStatus x
-             then execWrapper master nl il cref jss
-             else do
-               hPutStrLn stderr $ "Not all jobs completed successfully: " ++
-                         show x
-               hPutStrLn stderr "Aborting."
-               return False)
-
--- | Signal handler for graceful termination
+  case jrs of
+    Bad x -> do
+      hPutStrLn stderr $ "Cannot compute job status, aborting: " ++ show x
+      return False
+    Ok x -> if checkJobsStatus x
+              then execWrapper master nl il cref jss
+              else do
+                hPutStrLn stderr $ "Not all jobs completed successfully: " ++
+                          show x
+                hPutStrLn stderr "Aborting."
+                return False
+
+-- | Executes the jobs, if possible and desired.
+maybeExecJobs :: Options
+              -> [a]
+              -> Node.List
+              -> Instance.List
+              -> [JobSet]
+              -> IO Bool
+maybeExecJobs opts ord_plc fin_nl il cmd_jobs =
+  if optExecJobs opts && not (null ord_plc)
+    then (case optLuxi opts of
+            Nothing -> do
+              hPutStrLn stderr "Execution of commands possible only on LUXI"
+              return False
+            Just master -> runJobSet master fin_nl il cmd_jobs)
+    else return True
+
+-- | Signal handler for graceful termination.
 hangleSigInt :: IORef Int -> IO ()
 hangleSigInt cref = do
   writeIORef cref 1
   putStrLn ("Cancel request registered, will exit at" ++
             " the end of the current job set...")
 
--- | Signal handler for immediate termination
+-- | Signal handler for immediate termination.
 hangleSigTerm :: IORef Int -> IO ()
 hangleSigTerm cref = do
   -- update the cref to 2, just for consistency
@@ -216,6 +245,7 @@ hangleSigTerm cref = do
   putStrLn "Double cancel request, exiting now..."
   exitImmediately $ ExitFailure 2
 
+-- | Runs a job set with handling of signals.
 runJobSet :: String -> Node.List -> Instance.List -> [JobSet] -> IO Bool
 runJobSet master fin_nl il cmd_jobs = do
   cref <- newIORef 0
@@ -223,61 +253,10 @@ runJobSet master fin_nl il cmd_jobs = do
     [(hangleSigTerm, softwareTermination), (hangleSigInt, keyboardSignal)]
   execWrapper master fin_nl il cref cmd_jobs
 
--- | Main function.
-main :: IO ()
-main = do
-  cmd_args <- System.getArgs
-  (opts, args) <- parseOpts cmd_args "hbal" options
-
-  unless (null args) $ do
-         hPutStrLn stderr "Error: this program doesn't take any arguments."
-         exitWith $ ExitFailure 1
-
-  let oneline = optOneline opts
-      verbose = optVerbose opts
-      shownodes = optShowNodes opts
-      showinsts = optShowInsts opts
-
-  ini_cdata@(ClusterData gl fixed_nl ilf ctags) <- loadExternalData opts
-
-  let offline_passed = optOffline opts
-      all_nodes = Container.elems fixed_nl
-      offline_lkp = map (lookupName (map Node.name all_nodes)) offline_passed
-      offline_wrong = filter (not . goodLookupResult) offline_lkp
-      offline_names = map lrContent offline_lkp
-      offline_indices = map Node.idx $
-                        filter (\n -> Node.name n `elem` offline_names)
-                               all_nodes
-      m_cpu = optMcpu opts
-      m_dsk = optMdsk opts
-      csf = commonSuffix fixed_nl ilf
-
-  when (not (null offline_wrong)) $ do
-         hPrintf stderr "Error: Wrong node name(s) set as offline: %s\n"
-                     (commaJoin (map lrContent offline_wrong)) :: IO ()
-         exitWith $ ExitFailure 1
-
-  let nm = Container.map (\n -> if Node.idx n `elem` offline_indices
-                                then Node.setOffline n True
-                                else n) fixed_nl
-      nlf = Container.map (flip Node.setMdsk m_dsk . flip Node.setMcpu m_cpu)
-            nm
-
-  when (not oneline && verbose > 1) $
-       putStrLn $ "Loaded cluster tags: " ++ intercalate "," ctags
-
-  when (Container.size ilf == 0) $ do
-         (if oneline then putStrLn $ formatOneline 0 0 0
-          else printf "Cluster is empty, exiting.\n")
-         exitWith ExitSuccess
-
-  let split_insts = Cluster.findSplitInstances nlf ilf
-  when (not . null $ split_insts) $ do
-    hPutStrLn stderr "Found instances belonging to multiple node groups:"
-    mapM_ (\i -> hPutStrLn stderr $ "  " ++ Instance.name i) split_insts
-    hPutStrLn stderr "Aborting."
-    exitWith $ ExitFailure 1
-
+-- | Select the target node group.
+selectGroup :: Options -> Group.List -> Node.List -> Instance.List
+            -> IO (String, (Node.List, Instance.List))
+selectGroup opts gl nlf ilf = do
   let ngroups = Cluster.splitCluster nlf ilf
   when (length ngroups > 1 && isNothing (optGroup opts)) $ do
     hPutStrLn stderr "Found multiple node groups:"
@@ -286,17 +265,11 @@ main = do
     hPutStrLn stderr "Aborting."
     exitWith $ ExitFailure 1
 
-  maybeSaveData (optSaveCluster opts) "original" "before balancing" ini_cdata
-
-  unless oneline $ printf "Loaded %d nodes, %d instances\n"
-             (Container.size nlf)
-             (Container.size ilf)
-
-  (gname, (nl, il)) <- case optGroup opts of
+  case optGroup opts of
     Nothing -> do
-         let (gidx, cdata) = head ngroups
-             grp = Container.find gidx gl
-         return (Group.name grp, cdata)
+      let (gidx, cdata) = head ngroups
+          grp = Container.find gidx gl
+      return (Group.name grp, cdata)
     Just g -> case Container.findByName gl g of
       Nothing -> do
         hPutStrLn stderr $ "Node group " ++ g ++
@@ -306,24 +279,47 @@ main = do
         exitWith $ ExitFailure 1
       Just grp ->
           case lookup (Group.idx grp) ngroups of
-            Nothing -> do
-              -- TODO: while this is unlikely to happen, log here the
-              -- actual group data to help debugging
-              hPutStrLn stderr "Internal failure, missing group idx"
-              exitWith $ ExitFailure 1
+            Nothing ->
+              -- This will only happen if there are no nodes assigned
+              -- to this group
+              return (Group.name grp, (Container.empty, Container.empty))
             Just cdata -> return (Group.name grp, cdata)
 
-  unless oneline $ printf "Group size %d nodes, %d instances\n"
-             (Container.size nl)
-             (Container.size il)
+-- | Do a few checks on the cluster data.
+checkCluster :: Int -> Node.List -> Instance.List -> IO ()
+checkCluster verbose nl il = do
+  -- nothing to do on an empty cluster
+  when (Container.null il) $ do
+         printf "Cluster is empty, exiting.\n"::IO ()
+         exitWith ExitSuccess
 
-  putStrLn $ "Selected node group: " ++ gname
+  -- hbal doesn't currently handle split clusters
+  let split_insts = Cluster.findSplitInstances nl il
+  unless (null split_insts) $ do
+    hPutStrLn stderr "Found instances belonging to multiple node groups:"
+    mapM_ (\i -> hPutStrLn stderr $ "  " ++ Instance.name i) split_insts
+    hPutStrLn stderr "Aborting."
+    exitWith $ ExitFailure 1
 
-  when (length csf > 0 && not oneline && verbose > 1) $
+  printf "Loaded %d nodes, %d instances\n"
+             (Container.size nl)
+             (Container.size il)::IO ()
+
+  let csf = commonSuffix nl il
+  when (not (null csf) && verbose > 1) $
        printf "Note: Stripping common suffix of '%s' from names\n" csf
 
+-- | Do a few checks on the selected group data.
+checkGroup :: Int -> String -> Node.List -> Instance.List -> IO ()
+checkGroup verbose gname nl il = do
+  printf "Group size %d nodes, %d instances\n"
+             (Container.size nl)
+             (Container.size il)::IO ()
+
+  putStrLn $ "Selected node group: " ++ gname
+
   let (bad_nodes, bad_instances) = Cluster.computeBadItems nl il
-  unless (oneline || verbose == 0) $ printf
+  unless (verbose == 0) $ printf
              "Initial check done: %d bad nodes, %d bad instances.\n"
              (length bad_nodes) (length bad_instances)
 
@@ -331,6 +327,42 @@ main = do
          putStrLn "Cluster is not N+1 happy, continuing but no guarantee \
                   \that the cluster will end N+1 happy."
 
+-- | Check that we actually need to rebalance.
+checkNeedRebalance :: Options -> Score -> IO ()
+checkNeedRebalance opts ini_cv = do
+  let min_cv = optMinScore opts
+  when (ini_cv < min_cv) $ do
+         printf "Cluster is already well balanced (initial score %.6g,\n\
+                \minimum score %.6g).\nNothing to do, exiting\n"
+                ini_cv min_cv:: IO ()
+         exitWith ExitSuccess
+
+-- | Main function.
+main :: Options -> [String] -> IO ()
+main opts args = do
+  unless (null args) $ do
+         hPutStrLn stderr "Error: this program doesn't take any arguments."
+         exitWith $ ExitFailure 1
+
+  let verbose = optVerbose opts
+      shownodes = optShowNodes opts
+      showinsts = optShowInsts opts
+
+  ini_cdata@(ClusterData gl fixed_nl ilf ctags ipol) <- loadExternalData opts
+
+  when (verbose > 1) $ do
+       putStrLn $ "Loaded cluster tags: " ++ intercalate "," ctags
+       putStrLn $ "Loaded cluster ipolicy: " ++ show ipol
+
+  nlf <- setNodeStatus opts fixed_nl
+  checkCluster verbose nlf ilf
+
+  maybeSaveData (optSaveCluster opts) "original" "before balancing" ini_cdata
+
+  (gname, (nl, il)) <- selectGroup opts gl nlf ilf
+
+  checkGroup verbose gname nl il
+
   maybePrintInsts showinsts "Initial" (Cluster.printInsts nl il)
 
   maybePrintNodes shownodes "Initial cluster" (Cluster.printNodes nl)
@@ -339,28 +371,21 @@ main = do
       ini_tbl = Cluster.Table nl il ini_cv []
       min_cv = optMinScore opts
 
-  when (ini_cv < min_cv) $ do
-         (if oneline then
-              putStrLn $ formatOneline ini_cv 0 ini_cv
-          else printf "Cluster is already well balanced (initial score %.6g,\n\
-                      \minimum score %.6g).\nNothing to do, exiting\n"
-                      ini_cv min_cv)
-         exitWith ExitSuccess
+  checkNeedRebalance opts ini_cv
 
-  unless oneline (if verbose > 2 then
-                      printf "Initial coefficients: overall %.8f, %s\n"
-                      ini_cv (Cluster.printStats nl)
-                  else
-                      printf "Initial score: %.8f\n" ini_cv)
+  if verbose > 2
+    then printf "Initial coefficients: overall %.8f\n%s"
+           ini_cv (Cluster.printStats "  " nl)::IO ()
+    else printf "Initial score: %.8f\n" ini_cv
 
-  unless oneline $ putStrLn "Trying to minimize the CV..."
+  putStrLn "Trying to minimize the CV..."
   let imlen = maximum . map (length . Instance.alias) $ Container.elems il
       nmlen = maximum . map (length . Node.alias) $ Container.elems nl
 
   (fin_tbl, cmd_strs) <- iterateDepth ini_tbl (optMaxLength opts)
                          (optDiskMoves opts)
                          (optInstMoves opts)
-                         nmlen imlen [] oneline min_cv
+                         nmlen imlen [] min_cv
                          (optMinGainLim opts) (optMinGain opts)
                          (optEvacMode opts)
   let (Cluster.Table fin_nl fin_il fin_cv fin_plc) = fin_tbl
@@ -368,56 +393,30 @@ main = do
       sol_msg = case () of
                   _ | null fin_plc -> printf "No solution found\n"
                     | verbose > 2 ->
-                        printf "Final coefficients:   overall %.8f, %s\n"
-                        fin_cv (Cluster.printStats fin_nl)
+                        printf "Final coefficients:   overall %.8f\n%s"
+                        fin_cv (Cluster.printStats "  " fin_nl)
                     | otherwise ->
                         printf "Cluster score improved from %.8f to %.8f\n"
                         ini_cv fin_cv ::String
 
-  unless oneline $ putStr sol_msg
+  putStr sol_msg
 
-  unless (oneline || verbose == 0) $
+  unless (verbose == 0) $
          printf "Solution length=%d\n" (length ord_plc)
 
   let cmd_jobs = Cluster.splitJobs cmd_strs
-      cmd_data = Cluster.formatCmds cmd_jobs
 
   when (isJust $ optShowCmds opts) $
-       do
-         let out_path = fromJust $ optShowCmds opts
-         putStrLn ""
-         (if out_path == "-" then
-              printf "Commands to run to reach the above solution:\n%s"
-                     (unlines . map ("  " ++) .
-                      filter (/= "  check") .
-                      lines $ cmd_data)
-          else do
-            writeFile out_path (shTemplate ++ cmd_data)
-            printf "The commands have been written to file '%s'\n" out_path)
+       saveBalanceCommands opts $ Cluster.formatCmds cmd_jobs
 
   maybeSaveData (optSaveCluster opts) "balanced" "after balancing"
-                (ClusterData gl fin_nl fin_il ctags)
+                ini_cdata { cdNodes = fin_nl, cdInstances = fin_il }
 
   maybePrintInsts showinsts "Final" (Cluster.printInsts fin_nl fin_il)
 
   maybePrintNodes shownodes "Final cluster" (Cluster.printNodes fin_nl)
 
-  when (verbose > 3) $ do
-         let ini_cs = Cluster.totalResources nl
-             fin_cs = Cluster.totalResources fin_nl
-         printf "Original: mem=%d disk=%d\n"
-                    (Cluster.csFmem ini_cs) (Cluster.csFdsk ini_cs) :: IO ()
-         printf "Final:    mem=%d disk=%d\n"
-                    (Cluster.csFmem fin_cs) (Cluster.csFdsk fin_cs)
-  when oneline $
-         putStrLn $ formatOneline ini_cv (length ord_plc) fin_cv
-
-  eval <-
-      if optExecJobs opts && not (null ord_plc)
-      then (case optLuxi opts of
-              Nothing -> do
-                hPutStrLn stderr "Execution of commands possible only on LUXI"
-                return False
-              Just master -> runJobSet master fin_nl il cmd_jobs)
-      else return True
+  when (verbose > 3) $ printStats nl fin_nl
+
+  eval <- maybeExecJobs opts ord_plc fin_nl il cmd_jobs
   unless eval (exitWith (ExitFailure 1))