module Ganeti.Confd.Server
( main
, checkMain
, prepMain
) where
import Control.Concurrent
import Control.Exception
import Control.Monad (forever, liftM, unless)
import Data.IORef
import Data.List
import qualified Data.Map as M
import Data.Maybe (fromMaybe)
import qualified Network.Socket as S
import System.Exit
import System.IO
import System.Posix.Files
import System.Posix.Types
import qualified Text.JSON as J
import System.INotify
import Ganeti.BasicTypes
import Ganeti.Errors
import Ganeti.Daemon
import Ganeti.JSON
import Ganeti.Objects
import Ganeti.Confd.Types
import Ganeti.Confd.Utils
import Ganeti.Config
import Ganeti.Hash
import Ganeti.Logging
import qualified Ganeti.Constants as C
import qualified Ganeti.Path as Path
import Ganeti.Query.Server (prepQueryD, runQueryD)
import Ganeti.Utils
type CRef = IORef (Result (ConfigData, LinkIpMap))
type FStat = (EpochTime, FileID, FileOffset)
nullFStat :: FStat
nullFStat = (1, 1, 1)
type StatusAnswer = (ConfdReplyStatus, J.JSValue)
data ReloadModel = ReloadNotify
| ReloadPoll Int
deriving (Eq, Show)
data ServerState = ServerState
{ reloadModel :: ReloadModel
, reloadTime :: Integer
, reloadFStat :: FStat
}
maxIdlePollRounds :: Int
maxIdlePollRounds = 3
watchInterval :: Int
watchInterval = C.confdConfigReloadTimeout * 1000000
pollInterval :: Int
pollInterval = C.confdConfigReloadRatelimit
reloadRatelimit :: Integer
reloadRatelimit = fromIntegral C.confdConfigReloadRatelimit
initialPoll :: ReloadModel
initialPoll = ReloadPoll 0
data ConfigReload = ConfigToDate
| ConfigReloaded
| ConfigIOError
deriving (Eq)
queryUnknownEntry :: StatusAnswer
queryUnknownEntry = (ReplyStatusError, J.showJSON ConfdErrorUnknownEntry)
queryArgumentError :: StatusAnswer
queryArgumentError = (ReplyStatusError, J.showJSON ConfdErrorArgument)
gntErrorToResult :: ErrorResult a -> Result a
gntErrorToResult (Bad err) = Bad (show err)
gntErrorToResult (Ok x) = Ok x
nodeRole :: ConfigData -> String -> Result ConfdNodeRole
nodeRole cfg name =
let cmaster = clusterMasterNode . configCluster $ cfg
mnode = M.lookup name . fromContainer . configNodes $ cfg
in case mnode of
Nothing -> Bad "Node not found"
Just node | cmaster == name -> Ok NodeRoleMaster
| nodeDrained node -> Ok NodeRoleDrained
| nodeOffline node -> Ok NodeRoleOffline
| nodeMasterCandidate node -> Ok NodeRoleCandidate
_ -> Ok NodeRoleRegular
getNodePipByInstanceIp :: ConfigData
-> LinkIpMap
-> String
-> String
-> StatusAnswer
getNodePipByInstanceIp cfg linkipmap link instip =
case M.lookup instip (M.findWithDefault M.empty link linkipmap) of
Nothing -> queryUnknownEntry
Just instname ->
case getInstPrimaryNode cfg instname of
Bad _ -> queryUnknownEntry
Ok node -> (ReplyStatusOk, J.showJSON (nodePrimaryIp node))
buildResponse :: (ConfigData, LinkIpMap) -> ConfdRequest -> Result StatusAnswer
buildResponse (cfg, _) (ConfdRequest { confdRqType = ReqPing }) =
return (ReplyStatusOk, J.showJSON (configVersion cfg))
buildResponse cdata req@(ConfdRequest { confdRqType = ReqClusterMaster }) =
case confdRqQuery req of
EmptyQuery -> return (ReplyStatusOk, J.showJSON master_name)
PlainQuery _ -> return queryArgumentError
DictQuery reqq -> do
mnode <- gntErrorToResult $ getNode cfg master_name
let fvals = map (\field -> case field of
ReqFieldName -> master_name
ReqFieldIp -> clusterMasterIp cluster
ReqFieldMNodePip -> nodePrimaryIp mnode
) (confdReqQFields reqq)
return (ReplyStatusOk, J.showJSON fvals)
where master_name = clusterMasterNode cluster
cluster = configCluster cfg
cfg = fst cdata
buildResponse cdata req@(ConfdRequest { confdRqType = ReqNodeRoleByName }) = do
node_name <- case confdRqQuery req of
PlainQuery str -> return str
_ -> fail $ "Invalid query type " ++ show (confdRqQuery req)
role <- nodeRole (fst cdata) node_name
return (ReplyStatusOk, J.showJSON role)
buildResponse cdata (ConfdRequest { confdRqType = ReqNodePipList }) =
return (ReplyStatusOk, J.showJSON $
M.foldlWithKey (\accu _ n -> nodePrimaryIp n:accu) []
(fromContainer . configNodes . fst $ cdata))
buildResponse cdata (ConfdRequest { confdRqType = ReqMcPipList }) =
return (ReplyStatusOk, J.showJSON $
M.foldlWithKey (\accu _ n -> if nodeMasterCandidate n
then nodePrimaryIp n:accu
else accu) []
(fromContainer . configNodes . fst $ cdata))
buildResponse (cfg, linkipmap)
req@(ConfdRequest { confdRqType = ReqInstIpsList }) = do
link <- case confdRqQuery req of
PlainQuery str -> return str
EmptyQuery -> return (getDefaultNicLink cfg)
_ -> fail "Invalid query type"
return (ReplyStatusOk, J.showJSON $ getInstancesIpByLink linkipmap link)
buildResponse cdata (ConfdRequest { confdRqType = ReqNodePipByInstPip
, confdRqQuery = DictQuery query}) =
let (cfg, linkipmap) = cdata
link = fromMaybe (getDefaultNicLink cfg) (confdReqQLink query)
in case confdReqQIp query of
Just ip -> return $ getNodePipByInstanceIp cfg linkipmap link ip
Nothing -> return (ReplyStatusOk,
J.showJSON $
map (getNodePipByInstanceIp cfg linkipmap link)
(confdReqQIpList query))
buildResponse _ (ConfdRequest { confdRqType = ReqNodePipByInstPip }) =
return queryArgumentError
buildResponse cdata req@(ConfdRequest { confdRqType = ReqNodeDrbd }) = do
let cfg = fst cdata
node_name <- case confdRqQuery req of
PlainQuery str -> return str
_ -> fail $ "Invalid query type " ++ show (confdRqQuery req)
node <- gntErrorToResult $ getNode cfg node_name
let minors = concatMap (getInstMinorsForNode (nodeName node)) .
M.elems . fromContainer . configInstances $ cfg
encoded = [J.JSArray [J.showJSON a, J.showJSON b, J.showJSON c,
J.showJSON d, J.showJSON e, J.showJSON f] |
(a, b, c, d, e, f) <- minors]
return (ReplyStatusOk, J.showJSON encoded)
serializeResponse :: Result StatusAnswer -> ConfdReply
serializeResponse r =
let (status, result) = case r of
Bad err -> (ReplyStatusError, J.showJSON err)
Ok (code, val) -> (code, val)
in ConfdReply { confdReplyProtocol = 1
, confdReplyStatus = status
, confdReplyAnswer = result
, confdReplySerial = 0 }
moveToPolling :: String -> INotify -> FilePath -> CRef -> MVar ServerState
-> IO ReloadModel
moveToPolling msg inotify path cref mstate = do
logInfo $ "Moving to polling mode: " ++ msg
let inotiaction = addNotifier inotify path cref mstate
_ <- forkIO $ onPollTimer inotiaction path cref mstate
return initialPoll
moveToNotify :: IO ReloadModel
moveToNotify = do
logInfo "Moving to inotify mode"
return ReloadNotify
updateConfig :: FilePath -> CRef -> IO ()
updateConfig path r = do
newcfg <- loadConfig path
let !newdata = case newcfg of
Ok !cfg -> Ok (cfg, buildLinkIpInstnameMap cfg)
Bad _ -> Bad "Cannot load configuration"
writeIORef r newdata
case newcfg of
Ok cfg -> logInfo ("Loaded new config, serial " ++
show (configSerial cfg))
Bad msg -> logError $ "Failed to load config: " ++ msg
return ()
safeUpdateConfig :: FilePath -> FStat -> CRef -> IO (FStat, ConfigReload)
safeUpdateConfig path oldfstat cref =
Control.Exception.catch
(do
nt <- needsReload oldfstat path
case nt of
Nothing -> return (oldfstat, ConfigToDate)
Just nt' -> do
updateConfig path cref
return (nt', ConfigReloaded)
) (\e -> do
let msg = "Failure during configuration update: " ++
show (e::IOError)
writeIORef cref (Bad msg)
return (nullFStat, ConfigIOError)
)
buildFileStatus :: FileStatus -> FStat
buildFileStatus ofs =
let modt = modificationTime ofs
inum = fileID ofs
fsize = fileSize ofs
in (modt, inum, fsize)
getFStat :: FilePath -> IO FStat
getFStat p = liftM buildFileStatus (getFileStatus p)
needsReload :: FStat -> FilePath -> IO (Maybe FStat)
needsReload oldstat path = do
newstat <- getFStat path
return $ if newstat /= oldstat
then Just newstat
else Nothing
onWatcherTimer :: IO Bool -> FilePath -> CRef -> MVar ServerState -> IO ()
onWatcherTimer inotiaction path cref state = do
threadDelay watchInterval
logDebug "Watcher timer fired"
modifyMVar_ state (onWatcherInner path cref)
_ <- inotiaction
onWatcherTimer inotiaction path cref state
onWatcherInner :: FilePath -> CRef -> ServerState -> IO ServerState
onWatcherInner path cref state = do
(newfstat, _) <- safeUpdateConfig path (reloadFStat state) cref
return state { reloadFStat = newfstat }
onPollTimer :: IO Bool -> FilePath -> CRef -> MVar ServerState -> IO ()
onPollTimer inotiaction path cref state = do
threadDelay pollInterval
logDebug "Poll timer fired"
continue <- modifyMVar state (onPollInner inotiaction path cref)
if continue
then onPollTimer inotiaction path cref state
else logDebug "Inotify watch active, polling thread exiting"
onPollInner :: IO Bool -> FilePath -> CRef -> ServerState
-> IO (ServerState, Bool)
onPollInner _ _ _ state@(ServerState { reloadModel = ReloadNotify } ) =
return (state, False)
onPollInner inotiaction path cref
state@(ServerState { reloadModel = ReloadPoll pround } ) = do
(newfstat, reload) <- safeUpdateConfig path (reloadFStat state) cref
let state' = state { reloadFStat = newfstat }
newmode <- case reload of
ConfigToDate ->
if pround >= maxIdlePollRounds
then do
result <- inotiaction
if result
then moveToNotify
else return initialPoll
else return (ReloadPoll (pround + 1))
_ -> return initialPoll
let continue = case newmode of
ReloadNotify -> False
_ -> True
return (state' { reloadModel = newmode }, continue)
addNotifier :: INotify -> FilePath -> CRef -> MVar ServerState -> IO Bool
addNotifier inotify path cref mstate =
Control.Exception.catch
(addWatch inotify [CloseWrite] path
(onInotify inotify path cref mstate) >> return True)
(\e -> const (return False) (e::IOError))
onInotify :: INotify -> String -> CRef -> MVar ServerState -> Event -> IO ()
onInotify inotify path cref mstate Ignored = do
logDebug "File lost, trying to re-establish notifier"
modifyMVar_ mstate $ \state -> do
result <- addNotifier inotify path cref mstate
(newfstat, _) <- safeUpdateConfig path (reloadFStat state) cref
let state' = state { reloadFStat = newfstat }
if result
then return state'
else do
mode <- moveToPolling "cannot re-establish inotify watch" inotify
path cref mstate
return state' { reloadModel = mode }
onInotify inotify path cref mstate _ =
modifyMVar_ mstate $ \state ->
if reloadModel state == ReloadNotify
then do
ctime <- getCurrentTimeUSec
(newfstat, _) <- safeUpdateConfig path (reloadFStat state) cref
let state' = state { reloadFStat = newfstat, reloadTime = ctime }
if abs (reloadTime state ctime) < reloadRatelimit
then do
mode <- moveToPolling "too many reloads" inotify path cref mstate
return state' { reloadModel = mode }
else return state'
else return state
responder :: CRef -> S.Socket -> HashKey -> String -> S.SockAddr -> IO ()
responder cfgref socket hmac msg peer = do
ctime <- getCurrentTime
case parseRequest hmac msg ctime of
Ok (origmsg, rq) -> do
logDebug $ "Processing request: " ++ rStripSpace origmsg
mcfg <- readIORef cfgref
let response = respondInner mcfg hmac rq
_ <- S.sendTo socket response peer
return ()
Bad err -> logInfo $ "Failed to parse incoming message: " ++ err
return ()
respondInner :: Result (ConfigData, LinkIpMap) -> HashKey
-> ConfdRequest -> String
respondInner cfg hmac rq =
let rsalt = confdRqRsalt rq
innermsg = serializeResponse (cfg >>= flip buildResponse rq)
innerserialised = J.encodeStrict innermsg
outermsg = signMessage hmac rsalt innerserialised
outerserialised = confdMagicFourcc ++ J.encodeStrict outermsg
in outerserialised
listener :: S.Socket -> HashKey
-> (S.Socket -> HashKey -> String -> S.SockAddr -> IO ())
-> IO ()
listener s hmac resp = do
(msg, _, peer) <- S.recvFrom s 4096
if confdMagicFourcc `isPrefixOf` msg
then forkIO (resp s hmac (drop 4 msg) peer) >> return ()
else logDebug "Invalid magic code!" >> return ()
return ()
configReader :: CRef -> IO (Result ConfigData)
configReader cref = do
cdata <- readIORef cref
return $ liftM fst cdata
type PrepResult = (S.Socket, (FilePath, S.Socket),
IORef (Result (ConfigData, LinkIpMap)))
checkMain :: CheckFn (S.Family, S.SockAddr)
checkMain opts = do
parseresult <- parseAddress opts C.defaultConfdPort
case parseresult of
Bad msg -> do
hPutStrLn stderr $ "parsing bind address: " ++ msg
return . Left $ ExitFailure 1
Ok v -> return $ Right v
prepMain :: PrepFn (S.Family, S.SockAddr) PrepResult
prepMain _ (af_family, bindaddr) = do
s <- S.socket af_family S.Datagram S.defaultProtocol
S.bindSocket s bindaddr
query_data <- prepQueryD Nothing
cref <- newIORef (Bad "Configuration not yet loaded")
return (s, query_data, cref)
main :: MainFn (S.Family, S.SockAddr) PrepResult
main _ _ (s, query_data, cref) = do
inotify <- initINotify
conf_file <- Path.clusterConfFile
(fstat, reloaded) <- safeUpdateConfig conf_file nullFStat cref
ctime <- getCurrentTime
statemvar <- newMVar $ ServerState ReloadNotify ctime fstat
let inotiaction = addNotifier inotify conf_file cref statemvar
has_inotify <- if reloaded == ConfigReloaded
then inotiaction
else return False
if has_inotify
then logInfo "Starting up in inotify mode"
else do
logInfo "Starting up in polling mode"
modifyMVar_ statemvar
(\state -> return state { reloadModel = initialPoll })
hmac <- getClusterHmac
_ <- forkIO $ onWatcherTimer inotiaction conf_file cref statemvar
unless has_inotify $ do
_ <- forkIO $ onPollTimer inotiaction conf_file cref statemvar
return ()
_ <- forkIO $ runQueryD query_data (configReader cref)
forever $ listener s hmac (responder cref)