module Ganeti.HTools.Program.Hsqueeze
(main
, options
, arguments
) where
import Control.Applicative
import Control.Lens (over)
import Control.Monad
import Data.Function
import Data.List
import Data.Maybe
import qualified Data.IntMap as IntMap
import Text.Printf (printf)
import Ganeti.BasicTypes
import Ganeti.Common
import qualified Ganeti.HTools.AlgorithmParams as Alg
import Ganeti.HTools.CLI
import qualified Ganeti.HTools.Container as Container
import qualified Ganeti.HTools.Cluster as Cluster
import qualified Ganeti.HTools.Cluster.Metrics as Metrics
import Ganeti.HTools.ExtLoader
import qualified Ganeti.HTools.Instance as Instance
import Ganeti.HTools.Loader
import qualified Ganeti.HTools.Node as Node
import Ganeti.HTools.Tags (hasStandbyTag)
import Ganeti.HTools.Tags.Constants (standbyAuto)
import Ganeti.HTools.Types
import Ganeti.JQueue (currentTimestamp, reasonTrailTimestamp)
import Ganeti.JQueue.Objects (Timestamp)
import qualified Ganeti.Jobs as Jobs
import Ganeti.OpCodes
import Ganeti.OpCodes.Lens (metaParamsL, opReasonL)
import Ganeti.Utils
import Ganeti.Version (version)
options :: IO [OptType]
options = do
luxi <- oLuxiSocket
return
[ luxi
, oDataFile
, oExecJobs
, oMinResources
, oTargetResources
, oSaveCluster
, oPrintCommands
, oVerbose
, oNoHeaders
, oStaticKvmNodeMemory
]
arguments :: [ArgCompletion]
arguments = []
annotateOpCode :: Timestamp -> String -> Jobs.Annotator
annotateOpCode ts comment =
over (metaParamsL . opReasonL)
(++ [("hsqueeze"
, "hsqueeze " ++ version ++ " called"
, reasonTrailTimestamp ts
)])
. setOpComment (comment ++ " " ++ version)
. wrapOpCode
onlyExternal :: (Node.List, Instance.List) -> Node.Node -> Bool
onlyExternal (_, il) nd =
not
. any (Instance.usesLocalStorage . flip Container.find il)
$ Node.pList nd
noSecondaries :: Node.Node -> Bool
noSecondaries = null . Node.sList
allInstancesOnOnlineNodes :: (Node.List, Instance.List) -> Bool
allInstancesOnOnlineNodes (nl, il) =
all (not . Node.offline . flip Container.find nl . Instance.pNode)
. IntMap.elems
$ il
allNodesCapacityFor :: Instance.Instance -> (Node.List, Instance.List) -> Bool
allNodesCapacityFor inst (nl, _) =
all (isOk . flip Node.addPri inst) . IntMap.elems $ nl
balance :: (Node.List, Instance.List)
-> ((Node.List, Instance.List), [MoveJob])
balance (nl, il) =
let ini_cv = Metrics.compCV nl
ini_tbl = Cluster.Table nl il ini_cv []
balanceStep = Cluster.tryBalance
(Alg.defaultOptions { Alg.algMinGain = 0.0
, Alg.algMinGainLimit = 0.0})
bTables = map fromJust . takeWhile isJust
$ iterate (>>= balanceStep) (Just ini_tbl)
(Cluster.Table nl' il' _ _) = last bTables
moves = zip bTables (drop 1 bTables) >>= Cluster.getMoves
in ((nl', il'), reverse moves)
onlineOfflineNode :: Bool -> (Node.List, Instance.List) -> Ndx ->
(Node.List, Instance.List)
onlineOfflineNode offline (nl, il) ndx =
let nd = Container.find ndx nl
nd' = Node.setOffline nd offline
nl' = Container.add ndx nd' nl
in (nl', il)
onlineOfflineNodes :: Bool -> [Ndx] -> (Node.List, Instance.List)
-> ((Node.List, Instance.List), [MoveJob])
onlineOfflineNodes offline ndxs conf =
let conf' = foldl (onlineOfflineNode offline) conf ndxs
in balance conf'
offlineNodes :: [Ndx] -> (Node.List, Instance.List)
-> ((Node.List, Instance.List), [MoveJob])
offlineNodes = onlineOfflineNodes True
onlineNodes :: [Ndx] -> (Node.List, Instance.List)
-> ((Node.List, Instance.List), [MoveJob])
onlineNodes = onlineOfflineNodes False
canOnlineOffline :: Bool -> Instance.Instance -> (Node.List, Instance.List)
-> [Node.Node] ->Bool
canOnlineOffline offline inst conf nds =
let conf' = fst $ onlineOfflineNodes offline (map Node.idx nds) conf
in allInstancesOnOnlineNodes conf' && allNodesCapacityFor inst conf'
canOffline :: Instance.Instance -> (Node.List, Instance.List) ->
[Node.Node] -> Bool
canOffline = canOnlineOffline True
sufficesOnline :: Instance.Instance -> (Node.List, Instance.List)
-> [Node.Node] -> Bool
sufficesOnline = canOnlineOffline False
greedyOfflineNodes :: Instance.Instance -> (Node.List, Instance.List)
-> [Node.Node] -> [Node.Node]
greedyOfflineNodes _ _ [] = []
greedyOfflineNodes inst conf (nd:nds) =
let nds' = greedyOfflineNodes inst conf nds
in if canOffline inst conf (nd:nds') then nd:nds' else nds'
tryOnline :: Instance.Instance -> (Node.List, Instance.List) -> [Node.Node]
-> Maybe [Node.Node]
tryOnline inst conf = listToMaybe . filter (sufficesOnline inst conf) . inits
instanceFromSpecAndFactor :: String -> Double -> ISpec -> Instance.Instance
instanceFromSpecAndFactor name f spec =
Instance.create name
(floor (f * fromIntegral (iSpecMemorySize spec)))
0 []
(floor (f * fromIntegral (iSpecCpuCount spec)))
Running [] False Node.noSecondary Node.noSecondary DTExt
(floor (f * fromIntegral (iSpecSpindleUse spec)))
[]
False
getMoveOpCodes :: Node.List
-> Instance.List
-> [JobSet]
-> Result [([[OpCode]], String)]
getMoveOpCodes nl il js = return $ zip (map opcodes js) (map descr js)
where opcodes = map (\(_, idx, move, _) ->
Cluster.iMoveToJob nl il idx move)
descr job = "Moving instances " ++ commaJoin
(map (\(_, idx, _, _) -> Container.nameOf il idx) job)
getTagOpCodes :: [Node.Node] -> Result [([[OpCode]], String)]
getTagOpCodes nl = return $ zip (map opCode nl) (map descr nl)
where
opCode node = [[Node.genAddTagsOpCode node [standbyAuto]]]
descr node = "Tagging node " ++ Node.name node ++ " with standby"
getPowerOffOpCodes :: [Node.Node] -> Result [([[OpCode]], String)]
getPowerOffOpCodes nl = do
opcodes <- Node.genPowerOffOpCodes nl
return [([opcodes], "Powering off nodes")]
getPowerOnOpCodes :: [Node.Node] -> Result [([[OpCode]], String)]
getPowerOnOpCodes nl = do
opcodes <- Node.genPowerOnOpCodes nl
return [([opcodes], "Powering on nodes")]
maybeExecJobs :: Options
-> String
-> Result [([[OpCode]], String)]
-> IO (Result ())
maybeExecJobs opts comment opcodes =
if optExecJobs opts
then (case optLuxi opts of
Nothing ->
return $ Bad "Execution of commands possible only on LUXI"
Just master -> do
ts <- currentTimestamp
let annotator = maybe id setOpPriority (optPriority opts) .
annotateOpCode ts comment
case opcodes of
Bad msg -> error msg
Ok codes -> Jobs.execWithCancel annotator master codes)
else return $ Ok ()
main :: Options -> [String] -> IO ()
main opts args = do
unless (null args) $ exitErr "This program doesn't take any arguments."
let verbose = optVerbose opts
targetf = optTargetResources opts
minf = optMinResources opts
ini_cdata@(ClusterData _ nlf ilf _ ipol) <- loadExternalData opts
maybeSaveData (optSaveCluster opts) "original" "before hsqueeze run" ini_cdata
let nodelist = IntMap.elems nlf
offlineCandidates =
sortBy (flip compare `on` length . Node.pList)
. filter (foldl (liftA2 (&&)) (const True)
[ not . Node.offline
, not . Node.isMaster
, noSecondaries
, onlyExternal (nlf, ilf)
])
$ nodelist
onlineCandidates =
filter (liftA2 (&&) Node.offline hasStandbyTag) nodelist
conf = (nlf, ilf)
std = iPolicyStdSpec ipol
targetInstance = instanceFromSpecAndFactor "targetInstance" targetf std
minInstance = instanceFromSpecAndFactor "targetInstance" minf std
toOffline = greedyOfflineNodes targetInstance conf offlineCandidates
((fin_off_nl, fin_off_il), off_mvs) =
offlineNodes (map Node.idx toOffline) conf
final_off_cdata =
ini_cdata { cdNodes = fin_off_nl, cdInstances = fin_off_il }
off_jobs = Cluster.splitJobs off_mvs
off_opcodes = liftM concat $ sequence
[ getMoveOpCodes nlf ilf off_jobs
, getTagOpCodes toOffline
, getPowerOffOpCodes toOffline
]
off_cmd =
Cluster.formatCmds off_jobs
++ "\necho Tagging Commands\n"
++ (toOffline >>= (printf " gnt-node add-tags %s %s\n"
`flip` standbyAuto)
. Node.alias)
++ "\necho Power Commands\n"
++ (toOffline >>= printf " gnt-node power -f off %s\n" . Node.alias)
toOnline = tryOnline minInstance conf onlineCandidates
nodesToOnline = fromMaybe onlineCandidates toOnline
((fin_on_nl, fin_on_il), on_mvs) =
onlineNodes (map Node.idx nodesToOnline) conf
final_on_cdata =
ini_cdata { cdNodes = fin_on_nl, cdInstances = fin_on_il }
on_jobs = Cluster.splitJobs on_mvs
on_opcodes = liftM2 (++) (getPowerOnOpCodes nodesToOnline)
(getMoveOpCodes nlf ilf on_jobs)
on_cmd =
"echo Power Commands\n"
++ (nodesToOnline >>= printf " gnt-node power -f on %s\n" . Node.alias)
++ Cluster.formatCmds on_jobs
when (verbose > 1) . putStrLn
$ "Offline candidates: " ++ commaJoin (map Node.name offlineCandidates)
when (verbose > 1) . putStrLn
$ "Online candidates: " ++ commaJoin (map Node.name onlineCandidates)
if not (allNodesCapacityFor minInstance conf)
then do
unless (optNoHeaders opts) $
putStrLn "'Nodes to online'"
mapM_ (putStrLn . Node.name) nodesToOnline
when (verbose > 1 && isNothing toOnline) . putStrLn $
"Onlining all nodes will not yield enough capacity"
maybeSaveCommands "Commands to run:" opts on_cmd
let comment = printf "expanding by %d nodes" (length nodesToOnline)
exitIfBad "hsqueeze" =<< maybeExecJobs opts comment on_opcodes
maybeSaveData (optSaveCluster opts)
"squeezed" "after hsqueeze expansion" final_on_cdata
else
if null toOffline
then do
unless (optNoHeaders opts) $
putStrLn "'No action'"
maybeSaveCommands "Commands to run:" opts "echo Nothing to do"
maybeSaveData (optSaveCluster opts)
"squeezed" "after hsqueeze doing nothing" ini_cdata
else do
unless (optNoHeaders opts) $
putStrLn "'Nodes to offline'"
mapM_ (putStrLn . Node.name) toOffline
maybeSaveCommands "Commands to run:" opts off_cmd
let comment = printf "condensing by %d nodes" (length toOffline)
exitIfBad "hsqueeze" =<< maybeExecJobs opts comment off_opcodes
maybeSaveData (optSaveCluster opts)
"squeezed" "after hsqueeze run" final_off_cdata