module Ganeti.HTools.Backend.MonD
( queryAllMonDDCs
, pMonDData
) where
import Control.Monad
import Control.Monad.Writer
import qualified Data.List as L
import qualified Data.IntMap as IntMap
import qualified Data.Map as Map
import Data.Maybe (catMaybes, mapMaybe)
import qualified Data.Set as Set
import Network.Curl
import qualified Text.JSON as J
import Ganeti.BasicTypes
import qualified Ganeti.Constants as C
import Ganeti.Cpu.Types
import qualified Ganeti.DataCollectors.XenCpuLoad as XenCpuLoad
import qualified Ganeti.DataCollectors.CPUload as CPUload
import Ganeti.DataCollectors.Types ( DCReport, DCCategory
, dcReportData, dcReportName
, getCategoryName )
import qualified Ganeti.HTools.Container as Container
import qualified Ganeti.HTools.Node as Node
import qualified Ganeti.HTools.Instance as Instance
import Ganeti.HTools.Loader (ClusterData(..))
import Ganeti.HTools.Types
import Ganeti.HTools.CLI
import Ganeti.JSON
import Ganeti.Logging.Lifted (logWarning)
import Ganeti.Utils (exitIfBad)
data Report = CPUavgloadReport CPUavgload
| InstanceCpuReport (Map.Map String Double)
data DataCollector = DataCollector
{ dName :: String
, dCategory :: Maybe DCCategory
, dMkReport :: DCReport -> Maybe Report
, dUse :: [(Node.Node, Report)]
-> (Node.List, Instance.List)
-> Result (Node.List, Instance.List)
}
mkCpuReport :: DCReport -> Maybe Report
mkCpuReport dcr =
case fromJVal (dcReportData dcr) :: Result CPUavgload of
Ok cav -> Just $ CPUavgloadReport cav
Bad _ -> Nothing
updateNodeCpuFromReport :: (Node.Node, Report) -> Node.Node
updateNodeCpuFromReport (node, CPUavgloadReport cav) =
let ct = cavCpuTotal cav
du = Node.utilLoad node
du' = du {cpuWeight = ct}
in node { Node.utilLoad = du' }
updateNodeCpuFromReport (node, _) = node
updateCpuUtilDataFromNode :: Instance.List -> Node.Node -> Instance.List
updateCpuUtilDataFromNode il node =
let ct = cpuWeight (Node.utilLoad node)
n_uCpu = Node.uCpu node
upd inst =
if Node.idx node == Instance.pNode inst
then
let i_vcpus = Instance.vcpus inst
i_util = ct / fromIntegral n_uCpu * fromIntegral i_vcpus
i_du = Instance.util inst
i_du' = i_du {cpuWeight = i_util}
in inst {Instance.util = i_du'}
else inst
in Container.map upd il
useNodeTotalCPU :: [(Node.Node, Report)]
-> (Node.List, Instance.List)
-> Result (Node.List, Instance.List)
useNodeTotalCPU reports (nl, il) =
let newnodes = map updateNodeCpuFromReport reports
il' = foldl updateCpuUtilDataFromNode il newnodes
nl' = zip (Container.keys nl) newnodes
in return (Container.fromList nl', il')
totalCPUCollector :: DataCollector
totalCPUCollector = DataCollector { dName = CPUload.dcName
, dCategory = CPUload.dcCategory
, dMkReport = mkCpuReport
, dUse = useNodeTotalCPU
}
mkXenCpuReport :: DCReport -> Maybe Report
mkXenCpuReport =
liftM InstanceCpuReport . maybeParseMap . dcReportData
useInstanceCpuData :: [(Node.Node, Report)]
-> (Node.List, Instance.List)
-> Result (Node.List, Instance.List)
useInstanceCpuData reports (nl, il) = do
let toMap (InstanceCpuReport m) = Just m
toMap _ = Nothing
let usage = Map.unions $ mapMaybe (toMap . snd) reports
missingData = (Set.fromList . map Instance.name $ IntMap.elems il)
Set.\\ Map.keysSet usage
unless (Set.null missingData)
. Bad . (++) "No CPU information available for "
. show $ Set.elems missingData
let updateInstance inst =
let cpu = Map.lookup (Instance.name inst) usage
dynU = Instance.util inst
dynU' = maybe dynU (\c -> dynU { cpuWeight = c }) cpu
in inst { Instance.util = dynU' }
let il' = IntMap.map updateInstance il
let updateNode node =
let cpu = sum
. map (\ idx -> maybe 0 (cpuWeight . Instance.util)
$ IntMap.lookup idx il')
$ Node.pList node
dynU = Node.utilLoad node
dynU' = dynU { cpuWeight = cpu }
in node { Node.utilLoad = dynU' }
let nl' = IntMap.map updateNode nl
return (nl', il')
xenCPUCollector :: DataCollector
xenCPUCollector = DataCollector { dName = XenCpuLoad.dcName
, dCategory = XenCpuLoad.dcCategory
, dMkReport = mkXenCpuReport
, dUse = useInstanceCpuData
}
collectors :: Options -> [DataCollector]
collectors opts
| optIgnoreDynu opts = []
| optMonDXen opts = [ xenCPUCollector ]
| otherwise = [ totalCPUCollector ]
mkReport :: DataCollector -> Maybe DCReport -> Maybe Report
mkReport dc = (>>= dMkReport dc)
type MonDData = (String, [DCReport])
type MapMonDData = Map.Map String [DCReport]
fromFile :: DataCollector -> Node.Node -> MapMonDData -> Maybe DCReport
fromFile dc node m =
let matchDCName dcr = dName dc == dcReportName dcr
in maybe Nothing (L.find matchDCName) $ Map.lookup (Node.name node) m
getDCCName :: Maybe DCCategory -> String
getDCCName dcc =
case dcc of
Nothing -> "default"
Just c -> getCategoryName c
prepareUrl :: DataCollector -> Node.Node -> URLString
prepareUrl dc node =
Node.name node ++ ":" ++ show C.defaultMondPort ++ "/"
++ show C.mondLatestApiVersion ++ "/report/" ++
getDCCName (dCategory dc) ++ "/" ++ dName dc
fromCurl :: DataCollector -> Node.Node -> IO (Maybe DCReport)
fromCurl dc node = do
(code, !body) <- curlGetString (prepareUrl dc node) []
case code of
CurlOK ->
case J.decodeStrict body :: J.Result DCReport of
J.Ok r -> return $ Just r
J.Error _ -> return Nothing
_ -> do
logWarning $ "Failed to contact node's " ++ Node.name node
++ " MonD for DC " ++ dName dc
return Nothing
pMonDN :: JSRecord -> Result MonDData
pMonDN a = do
node <- tryFromObj "Parsing node's name" a "node"
reports <- tryFromObj "Parsing node's reports" a "reports"
return (node, reports)
pMonDData :: String -> Result [MonDData]
pMonDData input =
loadJSArray "Parsing MonD's answer" input >>=
mapM (pMonDN . J.fromJSObject)
queryAMonD :: Maybe MapMonDData -> DataCollector -> Node.Node
-> IO (Maybe Report)
queryAMonD m dc node =
liftM (mkReport dc) $ case m of
Nothing -> fromCurl dc node
Just m' -> return $ fromFile dc node m'
queryAllMonDs :: Maybe MapMonDData -> (Node.List, Instance.List)
-> DataCollector -> WriterT All IO (Node.List, Instance.List)
queryAllMonDs m (nl, il) dc = do
elems <- liftIO $ mapM (queryAMonD m dc) (Container.elems nl)
let elems' = catMaybes elems
if length elems == length elems'
then
let results = zip (Container.elems nl) elems'
in case dUse dc results (nl, il) of
Ok (nl', il') -> return (nl', il')
Bad s -> do
logWarning s
tell $ All False
return (nl, il)
else do
logWarning $ "Didn't receive an answer by all MonDs, " ++ dName dc
++ "'s data will be ignored."
tell $ All False
return (nl,il)
queryAllMonDDCs :: ClusterData -> Options -> WriterT All IO ClusterData
queryAllMonDDCs cdata opts = do
map_mDD <-
case optMonDFile opts of
Nothing -> return Nothing
Just fp -> do
monDData_contents <- liftIO $ readFile fp
monDData <- liftIO . exitIfBad "can't parse MonD data"
. pMonDData $ monDData_contents
return . Just $ Map.fromList monDData
let (ClusterData _ nl il _ _) = cdata
(nl', il') <- foldM (queryAllMonDs map_mDD) (nl, il) (collectors opts)
return $ cdata {cdNodes = nl', cdInstances = il'}