module Ganeti.Kvmd where
import Prelude hiding (rem)
import Control.Applicative ((<$>))
import Control.Exception (try)
import Control.Concurrent
import Control.Monad (unless, when)
import Data.List
import Data.Set (Set)
import qualified Data.Set as Set (delete, empty, insert, member)
import System.Directory
import System.FilePath
import System.IO
import System.IO.Error (isEOFError)
import System.INotify
import qualified AutoConf
import qualified Ganeti.BasicTypes as BasicTypes
import qualified Ganeti.Constants as Constants
import qualified Ganeti.Daemon as Daemon (getFQDN)
import qualified Ganeti.Logging as Logging
import qualified Ganeti.UDSServer as UDSServer
import qualified Ganeti.Ssconf as Ssconf
import qualified Ganeti.Types as Types
type Lock = MVar ()
type Monitors = MVar (Set FilePath)
isPrefixPath :: FilePath -> FilePath -> Bool
isPrefixPath x y =
(splitPath x `isPrefixOf` splitPath y) ||
(splitPath (x ++ "/") `isPrefixOf` splitPath y)
monitorGreeting :: String
monitorGreeting = "{\"execute\": \"qmp_capabilities\"}"
monitorDir :: String
monitorDir = AutoConf.localstatedir </> "run/ganeti/kvm-hypervisor/ctrl/"
monitorExtension :: String
monitorExtension = ".kvmd"
isMonitorPath :: FilePath -> Bool
isMonitorPath = (== monitorExtension) . takeExtension
shutdownExtension :: String
shutdownExtension = ".shutdown"
shutdownPath :: String -> String
shutdownPath = (`replaceExtension` shutdownExtension)
touchFile :: FilePath -> IO ()
touchFile file = withFile file WriteMode (const . return $ ())
parseQmp :: Bool -> Bool -> Bool -> String -> (Bool, Bool, Bool)
parseQmp isPowerdown isShutdown isStop str =
let
isPowerdown'
| "\"POWERDOWN\"" `isInfixOf` str = True
| otherwise = isPowerdown
isShutdown'
| "\"SHUTDOWN\"" `isInfixOf` str = True
| otherwise = isShutdown
isStop'
| "\"STOP\"" `isInfixOf` str = True
| otherwise = isStop
in
(isPowerdown', isShutdown', isStop')
receiveQmp :: Handle -> IO Bool
receiveQmp handle = isUserShutdown <$> receive False False False
where
isUserShutdown (isShutdown, isPowerdown, isStop)
= isPowerdown && not isShutdown && not isStop
receive isPowerdown isShutdown isStop =
do res <- try $ hGetLine handle
case res of
Left err -> do
unless (isEOFError err) $
hPrint stderr err
return (isPowerdown, isShutdown, isStop)
Right str -> do
let (isPowerdown', isShutdown', isStop') =
parseQmp isPowerdown isShutdown isStop str
Logging.logDebug $ "Receive QMP message: " ++ str
receive isPowerdown' isShutdown' isStop'
detectMonitor :: FilePath -> Handle -> IO ()
detectMonitor monitorFile handle =
do let shutdownFile = shutdownPath monitorFile
res <- receiveQmp handle
if res
then do
Logging.logInfo $ "Detect user shutdown, creating file " ++
show shutdownFile
touchFile shutdownFile
else do
Logging.logInfo $ "Detect admin shutdown, removing file " ++
show shutdownFile
(try (removeFile shutdownFile) :: IO (Either IOError ())) >> return ()
runMonitor :: FilePath -> IO ()
runMonitor monitorFile =
do handle <- UDSServer.openClientSocket Constants.luxiDefRwto monitorFile
hPutStrLn handle monitorGreeting
hFlush handle
detectMonitor monitorFile handle
UDSServer.closeClientSocket handle
ensureMonitor :: Monitors -> FilePath -> IO ()
ensureMonitor monitors monitorFile =
modifyMVar_ monitors $
\files ->
if monitorFile `Set.member` files
then return files
else do
forkIO tryMonitor >> return ()
return $ monitorFile `Set.insert` files
where tryMonitor =
do Logging.logInfo $ "Start monitor " ++ show monitorFile
res <- try (runMonitor monitorFile) :: IO (Either IOError ())
case res of
Left err ->
Logging.logError $ "Catch monitor exception: " ++ show err
_ ->
return ()
Logging.logInfo $ "Stop monitor " ++ show monitorFile
modifyMVar_ monitors (return . Set.delete monitorFile)
handleGenericEvent :: Lock -> String -> String -> Event -> IO ()
handleGenericEvent lock curDir tarDir ev@Created {}
| isDirectory ev && curDir /= tarDir &&
(curDir </> filePath ev) `isPrefixPath` tarDir = putMVar lock ()
handleGenericEvent lock _ _ event
| event == DeletedSelf || event == Unmounted = putMVar lock ()
handleGenericEvent _ _ _ _ = return ()
handleTargetEvent :: Lock -> Monitors -> String -> Event -> IO ()
handleTargetEvent _ monitors tarDir ev@Created {}
| not (isDirectory ev) && isMonitorPath (filePath ev) =
ensureMonitor monitors $ tarDir </> filePath ev
handleTargetEvent lock monitors tarDir ev@Opened {}
| not (isDirectory ev) =
case maybeFilePath ev of
Just p | isMonitorPath p ->
ensureMonitor monitors $ tarDir </> filePath ev
_ ->
handleGenericEvent lock tarDir tarDir ev
handleTargetEvent _ _ tarDir ev@Created {}
| not (isDirectory ev) && takeExtension (filePath ev) == shutdownExtension =
Logging.logInfo $ "User shutdown file opened " ++
show (tarDir </> filePath ev)
handleTargetEvent _ _ tarDir ev@Deleted {}
| not (isDirectory ev) && takeExtension (filePath ev) == shutdownExtension =
Logging.logInfo $ "User shutdown file deleted " ++
show (tarDir </> filePath ev)
handleTargetEvent lock _ tarDir ev =
handleGenericEvent lock tarDir tarDir ev
handleDir :: Lock -> Monitors -> String -> String -> Event -> IO ()
handleDir lock monitors curDir tarDir event =
do Logging.logDebug $ "Handle event " ++ show event
if curDir == tarDir
then handleTargetEvent lock monitors tarDir event
else handleGenericEvent lock curDir tarDir event
recapDir :: Lock -> Monitors -> FilePath -> IO ()
recapDir lock monitors dir =
do files <- getDirectoryContents dir
let files' = filter isMonitorPath files
mapM_ sendEvent files'
where sendEvent file =
handleTargetEvent lock monitors dir Created { isDirectory = False
, filePath = file }
watchDir :: Lock -> FilePath -> INotify -> IO ()
watchDir lock tarDir inotify = watchDir' tarDir
where watchDirEvents dir
| dir == tarDir = [AllEvents]
| otherwise = [Create, DeleteSelf]
watchDir' dir =
do add <- doesDirectoryExist dir
if add
then do
let events = watchDirEvents dir
Logging.logInfo $ "Watch directory " ++ show dir
monitors <- newMVar Set.empty
wd <- addWatch inotify events dir
(handleDir lock monitors dir tarDir)
when (dir == tarDir) $ recapDir lock monitors dir
() <- takeMVar lock
rem <- doesDirectoryExist dir
if rem
then do
Logging.logInfo $ "Unwatch directory " ++ show dir
removeWatch wd
else
Logging.logInfo $ "Throw away watch from directory " ++
show dir
else
watchDir' (takeDirectory dir)
rewatchDir :: Lock -> FilePath -> INotify -> IO ()
rewatchDir lock tarDir inotify =
do watchDir lock tarDir inotify
rewatchDir lock tarDir inotify
startWith :: FilePath -> IO ()
startWith dir =
do lock <- newEmptyMVar
withINotify (rewatchDir lock dir)
start :: IO ()
start =
do fqdn <- Daemon.getFQDN
hypervisors <- Ssconf.getHypervisorList Nothing
userShutdown <- Ssconf.getEnabledUserShutdown Nothing
vmCapable <- Ssconf.getNodesVmCapable Nothing
BasicTypes.genericResult
Logging.logInfo
(const $ startWith monitorDir) $ do
isKvm =<< hypervisors
isUserShutdown =<< userShutdown
isVmCapable fqdn =<< vmCapable
where
isKvm hs
| Types.Kvm `elem` hs = return ()
| otherwise = fail "KVM not enabled, exiting"
isUserShutdown True = return ()
isUserShutdown _ = fail "User shutdown not enabled, exiting"
isVmCapable node vmCapables =
case lookup node vmCapables of
Just True -> return ()
_ -> fail $ "Node " ++ show node ++ " is not VM capable, exiting"