blob: 668779b4959e3dc32a2558b786bd7506fa6e0bb9 [file] [log] [blame]
{-# LANGUAGE OverloadedStrings #-}
{-| Implementation of the Ganeti confd server functionality.
-}
{-
Copyright (C) 2013 Google Inc.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-}
module Ganeti.Monitoring.Server
( main
, checkMain
, prepMain
, DataCollector(..)
) where
import Prelude ()
import Ganeti.Prelude
import Control.Applicative
import Control.DeepSeq (force)
import Control.Exception.Base (evaluate)
import Control.Monad (void, forever, liftM, foldM, foldM_, mzero)
import Control.Monad.IO.Class
import Data.ByteString.Char8 (unpack)
import qualified Data.ByteString.UTF8 as UTF8
import Data.Maybe (fromMaybe)
import Data.List (find)
import qualified Data.Map as Map
import qualified Data.PSQueue as Queue
import Snap.Core
import Snap.Http.Server
import qualified Text.JSON as J
import Control.Concurrent
import qualified Ganeti.BasicTypes as BT
import Ganeti.Confd.Client
import Ganeti.Confd.Types
import qualified Ganeti.Confd.Types as CT
import Ganeti.Daemon
import qualified Ganeti.DataCollectors as DC
import Ganeti.DataCollectors.Types
import qualified Ganeti.JSON as GJ
import Ganeti.Objects (DataCollectorConfig(..))
import qualified Ganeti.Constants as C
import qualified Ganeti.ConstantUtils as CU
import Ganeti.Runtime
import Ganeti.Utils (getCurrentTimeUSec)
import Ganeti.Utils.Http (httpConfFromOpts, error404, plainJSON)
-- * Types and constants definitions
type ConfigAccess = String -> DataCollectorConfig
-- | Type alias for checkMain results.
type CheckResult = ()
-- | Type alias for prepMain results.
type PrepResult = Config Snap ()
-- | Version of the latest supported http API.
latestAPIVersion :: Int
latestAPIVersion = C.mondLatestApiVersion
-- * Helper functions
-- | Check function for the monitoring agent.
checkMain :: CheckFn CheckResult
checkMain _ = return $ Right ()
-- | Prepare function for monitoring agent.
prepMain :: PrepFn CheckResult PrepResult
prepMain opts _ = httpConfFromOpts GanetiMond opts
-- * Query answers
-- | Reply to the supported API version numbers query.
versionQ :: Snap ()
versionQ = plainJSON [latestAPIVersion]
-- | Version 1 of the monitoring HTTP API.
version1Api :: MVar CollectorMap -> MVar ConfigAccess -> Snap ()
version1Api mvar mvarConfig =
let returnNull = plainJSON J.JSNull
in ifTop returnNull <|>
route
[ ("list", listHandler mvarConfig)
, ("report", reportHandler mvar mvarConfig)
]
-- | Gives a lookup function for DataCollectorConfig that corresponds to the
-- configuration known to RConfD.
collectorConfigs :: ConfdClient -> IO ConfigAccess
collectorConfigs confdClient = do
response <- query confdClient CT.ReqDataCollectors CT.EmptyQuery
return $ lookupConfig response
where
lookupConfig :: Maybe ConfdReply -> String -> DataCollectorConfig
lookupConfig response name = fromMaybe (mempty :: DataCollectorConfig) $ do
confdReply <- response
let answer = CT.confdReplyAnswer confdReply
case J.readJSON answer :: J.Result (GJ.Container DataCollectorConfig) of
J.Error _ -> Nothing
J.Ok container -> GJ.lookupContainer Nothing (UTF8.fromString name)
container
activeCollectors :: MVar ConfigAccess -> IO [DataCollector]
activeCollectors mvarConfig = do
configs <- readMVar mvarConfig
return $ filter (dataCollectorActive . configs . dName) DC.collectors
-- | Get the JSON representation of a data collector to be used in the collector
-- list.
dcListItem :: DataCollector -> J.JSValue
dcListItem dc =
J.JSArray
[ J.showJSON $ dName dc
, maybe defaultCategory J.showJSON $ dCategory dc
, J.showJSON $ dKind dc
]
where
defaultCategory = J.showJSON C.mondDefaultCategory
-- | Handler for returning lists.
listHandler :: MVar ConfigAccess -> Snap ()
listHandler mvarConfig = dir "collectors" $ do
collectors' <- liftIO $ activeCollectors mvarConfig
plainJSON $ map dcListItem collectors'
-- | Handler for returning data collector reports.
reportHandler :: MVar CollectorMap -> MVar ConfigAccess -> Snap ()
reportHandler mvar mvarConfig =
route
[ ("all", allReports mvar mvarConfig)
, (":category/:collector", oneReport mvar mvarConfig)
] <|>
errorReport
-- | Return the report of all the available collectors.
allReports :: MVar CollectorMap -> MVar ConfigAccess -> Snap ()
allReports mvar mvarConfig = do
collectors' <- liftIO $ activeCollectors mvarConfig
reports <- mapM (liftIO . getReport mvar) collectors'
plainJSON reports
-- | Takes the CollectorMap and a DataCollector and returns the report for this
-- collector.
getReport :: MVar CollectorMap -> DataCollector -> IO DCReport
getReport mvar collector =
case dReport collector of
StatelessR r -> r
StatefulR r -> do
colData <- getColData (dName collector) mvar
r colData
-- | Returns the data for the corresponding collector.
getColData :: String -> MVar CollectorMap -> IO (Maybe CollectorData)
getColData name mvar = do
m <- readMVar mvar
return $ Map.lookup name m
-- | Returns a category given its name.
-- If "collector" is given as the name, the collector has no category, and
-- Nothing will be returned.
catFromName :: String -> BT.Result (Maybe DCCategory)
catFromName "instance" = BT.Ok $ Just DCInstance
catFromName "storage" = BT.Ok $ Just DCStorage
catFromName "daemon" = BT.Ok $ Just DCDaemon
catFromName "hypervisor" = BT.Ok $ Just DCHypervisor
catFromName "node" = BT.Ok $ Just DCNode
catFromName "default" = BT.Ok Nothing
catFromName _ = BT.Bad "No such category"
errorReport :: Snap ()
errorReport = do
modifyResponse $ setResponseStatus 404 "Not found"
writeBS "Unable to produce a report for the requested resource"
-- | Return the report of one collector.
oneReport :: MVar CollectorMap -> MVar ConfigAccess -> Snap ()
oneReport mvar mvarConfig = do
collectors' <- liftIO $ activeCollectors mvarConfig
categoryName <- maybe mzero unpack <$> getParam "category"
collectorName <- maybe mzero unpack <$> getParam "collector"
category <-
case catFromName categoryName of
BT.Ok cat -> return cat
BT.Bad msg -> fail msg
collector <-
case
find (\col -> collectorName == dName col) $
filter (\c -> category == dCategory c) collectors' of
Just col -> return col
Nothing -> fail "Unable to find the requested collector"
dcr <- liftIO $ getReport mvar collector
plainJSON dcr
-- | The function implementing the HTTP API of the monitoring agent.
monitoringApi :: MVar CollectorMap -> MVar ConfigAccess -> Snap ()
monitoringApi mvar mvarConfig =
ifTop versionQ <|>
dir "1" (version1Api mvar mvarConfig) <|>
error404
-- | The function collecting data for each data collector providing a dcUpdate
-- function.
collect :: CollectorMap -> DataCollector -> IO CollectorMap
collect m collector =
case dUpdate collector of
Nothing -> return m
Just update -> do
let name = dName collector
existing = Map.lookup name m
new_data <- update existing
_ <- evaluate $ force new_data
return $ Map.insert name new_data m
-- | Invokes collect for each data collector.
collection :: CollectorMap -> MVar ConfigAccess -> IO CollectorMap
collection m mvarConfig = do
collectors <- activeCollectors mvarConfig
foldM collect m collectors
-- | Convert seconds to microseconds
seconds :: Int -> Integer
seconds = (* 1000000) . fromIntegral
-- | The thread responsible for the periodical collection of data for each data
-- data collector. Note that even though the collectors might be deactivated,
-- they will still be collected to provide a complete history.
collectord :: MVar CollectorMap -> MVar ConfigAccess -> IO ()
collectord mvar mvarConfig = do
let queue = Queue.fromAscList . map (Queue.:-> 0)
$ CU.toList C.dataCollectorNames
foldM_ update queue [0::Integer ..]
where
resetTimer configs = Queue.adjustWithKey ((+) . dataCollectorInterval
. configs)
resetAll configs = foldr (resetTimer configs)
keyInList = flip . const . flip elem
update q _ = do
t <- getCurrentTimeUSec
configs <- readMVar mvarConfig
m <- takeMVar mvar
let dueNames = map Queue.key $ Queue.atMost t q
dueEntries = Map.filterWithKey (keyInList dueNames) m
m' <- collection dueEntries mvarConfig
let m'' = m' `Map.union` m
putMVar mvar m''
let q' = resetAll configs q dueNames
maxSleep = seconds C.mondTimeInterval
nextWakeup = fromMaybe maxSleep . liftM Queue.prio $ Queue.findMin q'
delay = min maxSleep nextWakeup
threadDelay $ fromInteger delay
return q'
-- | Main function.
main :: MainFn CheckResult PrepResult
main _ _ httpConf = do
mvarCollectorMap <- newMVar Map.empty
mvarConfig <- newEmptyMVar
confdClient <- getConfdClient Nothing Nothing
void . forkIO . forever $ do
configs <- collectorConfigs confdClient
putMVar mvarConfig configs
threadDelay . fromInteger $ seconds C.mondConfigTimeInterval
takeMVar mvarConfig
void . forkIO $ collectord mvarCollectorMap mvarConfig
httpServe httpConf . method GET $ monitoringApi mvarCollectorMap mvarConfig