module Ganeti.Utils
( debug
, debugFn
, debugXy
, sepSplit
, Statistics
, getSumStatistics
, getStdDevStatistics
, getStatisticValue
, updateStatistics
, stdDev
, if'
, select
, applyIf
, commaJoin
, ensureQuoted
, tryRead
, formatTable
, printTable
, parseUnit
, parseUnitAssumeBinary
, plural
, niceSort
, niceSortKey
, exitIfBad
, exitErr
, exitWhen
, exitUnless
, logWarningIfBad
, rStripSpace
, newUUID
, getCurrentTime
, getCurrentTimeUSec
, clockTimeToString
, chompPrefix
, warn
, wrap
, trim
, defaultHead
, exitIfEmpty
, splitEithers
, recombineEithers
, resolveAddr
, monadicThe
, setOwnerAndGroupFromNames
, formatOrdinal
, atomicWriteFile
, tryAndLogIOError
, lockFile
, FStat
, nullFStat
, getFStat
, getFStatSafe
, needsReload
, watchFile
, safeRenameFile
, FilePermissions(..)
, ensurePermissions
) where
import Control.Concurrent
import Control.Exception (try)
import Control.Monad (foldM, liftM, when, unless)
import Data.Char (toUpper, isAlphaNum, isDigit, isSpace)
import qualified Data.Either as E
import Data.Function (on)
import Data.IORef
import Data.List
import qualified Data.Map as M
import Numeric (showOct)
import System.Directory (renameFile, createDirectoryIfMissing)
import System.FilePath.Posix (takeDirectory, takeBaseName)
import System.INotify
import System.Posix.Types
import Debug.Trace
import Network.Socket
import Ganeti.BasicTypes
import qualified Ganeti.ConstantUtils as ConstantUtils
import Ganeti.Logging
import Ganeti.Runtime
import System.IO
import System.Exit
import System.Posix.Files
import System.Posix.IO
import System.Posix.User
import System.Time
debug :: Show a => a -> a
debug x = trace (show x) x
debugFn :: Show b => (a -> b) -> a -> a
debugFn fn x = debug (fn x) `seq` x
debugXy :: Show a => a -> b -> b
debugXy = seq . debug
applyIf :: Bool -> (a -> a) -> a -> a
applyIf b f x = if b then f x else x
commaJoin :: [String] -> String
commaJoin = intercalate ","
sepSplit :: Eq a => a -> [a] -> [[a]]
sepSplit sep s
| null s = []
| null xs = [x]
| null ys = [x,[]]
| otherwise = x:sepSplit sep ys
where (x, xs) = break (== sep) s
ys = drop 1 xs
plural :: Int -> String -> String -> String
plural 1 s _ = s
plural _ _ p = p
ensureQuoted :: String -> String
ensureQuoted v = if not (all (\c -> isAlphaNum c || c == '.') v)
then '\'':v ++ "'"
else v
stdDev :: [Double] -> Double
stdDev lst =
let (ll', sx) = foldl' (\(rl, rs) e ->
let rl' = rl + 1
rs' = rs + e
in rl' `seq` rs' `seq` (rl', rs')) (0::Int, 0) lst
ll = fromIntegral ll'::Double
mv = sx / ll
av = foldl' (\accu em -> let d = em mv in accu + d * d) 0.0 lst
in sqrt (av / ll)
data Statistics = SumStatistics Double
| StdDevStatistics Double Double Double deriving Show
getSumStatistics :: [Double] -> Statistics
getSumStatistics = SumStatistics . sum
getStdDevStatistics :: [Double] -> Statistics
getStdDevStatistics xs =
let (nt, st) = foldl' (\(n, s) x ->
let !n' = n + 1
!s' = s + x
in (n', s'))
(0, 0) xs
mean = st / nt
nvar = foldl' (\v x -> let d = x mean in v + d * d) 0 xs
in StdDevStatistics nt st (nvar / nt)
getStatisticValue :: Statistics -> Double
getStatisticValue (SumStatistics s) = s
getStatisticValue (StdDevStatistics _ _ var) = sqrt var
updateStatistics :: Statistics -> (Double, Double) -> Statistics
updateStatistics (SumStatistics s) (x, y) = SumStatistics $ s + (y x)
updateStatistics (StdDevStatistics n s var) (x, y) =
let !ds = y x
!dss = y * y x * x
!dnnvar = (n * dss 2 * s * ds) ds * ds
!s' = s + ds
!var' = max 0 $ var + dnnvar / (n * n)
in StdDevStatistics n s' var'
if' :: Bool
-> a
-> a
-> a
if' True x _ = x
if' _ _ y = y
parseChoices :: Monad m => String -> String -> [(a, String)] -> m a
parseChoices _ _ [(v, "")] = return v
parseChoices name s [(_, e)] =
fail $ name ++ ": leftover characters when parsing '"
++ s ++ "': '" ++ e ++ "'"
parseChoices name s _ = fail $ name ++ ": cannot parse string '" ++ s ++ "'"
tryRead :: (Monad m, Read a) => String -> String -> m a
tryRead name s = parseChoices name s $ reads s
formatTable :: [[String]] -> [Bool] -> [[String]]
formatTable vals numpos =
let vtrans = transpose vals
mlens = map (maximum . map length) vtrans
expnd = map (\(flds, isnum, ml) ->
map (\val ->
let delta = ml length val
filler = replicate delta ' '
in if delta > 0
then if isnum
then filler ++ val
else val ++ filler
else val
) flds
) (zip3 vtrans numpos mlens)
in transpose expnd
printTable :: String -> [String] -> [[String]] -> [Bool] -> String
printTable lp header rows isnum =
unlines . map ((++) lp . (:) ' ' . unwords) $
formatTable (header:rows) isnum
parseUnitValue :: (Monad m) => Bool -> String -> m Rational
parseUnitValue noDecimal unit
| null unit = return 1
| unit == "m" || upper == "MIB" = return 1
| unit == "g" || upper == "GIB" = return kbBinary
| unit == "t" || upper == "TIB" = return $ kbBinary * kbBinary
| unit == "M" || upper == "MB" = return mbFactor
| unit == "G" || upper == "GB" = return $ mbFactor * kbDecimal
| unit == "T" || upper == "TB" = return $ mbFactor * kbDecimal * kbDecimal
| otherwise = fail $ "Unknown unit '" ++ unit ++ "'"
where upper = map toUpper unit
kbBinary = 1024 :: Rational
kbDecimal = if noDecimal then kbBinary else 1000
decToBin = kbDecimal / kbBinary
mbFactor = decToBin * decToBin
parseUnitEx :: (Monad m, Integral a, Read a) => Bool -> String -> m a
parseUnitEx noDecimal str =
case (reads str::[(Int, String)]) of
[(v, suffix)] ->
let unit = dropWhile (== ' ') suffix
in do
scaling <- parseUnitValue noDecimal unit
return $ truncate (fromIntegral v * scaling)
_ -> fail $ "Can't parse string '" ++ str ++ "'"
parseUnit :: (Monad m, Integral a, Read a) => String -> m a
parseUnit = parseUnitEx False
parseUnitAssumeBinary :: (Monad m, Integral a, Read a) => String -> m a
parseUnitAssumeBinary = parseUnitEx True
exitIfBad :: String -> Result a -> IO a
exitIfBad msg (Bad s) = exitErr (msg ++ ": " ++ s)
exitIfBad _ (Ok v) = return v
exitErr :: String -> IO a
exitErr errmsg = do
hPutStrLn stderr $ "Error: " ++ errmsg
exitWith (ExitFailure 1)
exitWhen :: Bool -> String -> IO ()
exitWhen True msg = exitErr msg
exitWhen False _ = return ()
exitUnless :: Bool -> String -> IO ()
exitUnless cond = exitWhen (not cond)
logWarningIfBad :: String -> a -> Result a -> IO a
logWarningIfBad msg defVal (Bad s) = do
logWarning $ msg ++ ": " ++ s
return defVal
logWarningIfBad _ _ (Ok v) = return v
tryAndLogIOError :: IO a -> String -> (a -> Result b) -> IO (Result b)
tryAndLogIOError io msg okfn =
try io >>= either
(\ e -> do
let combinedmsg = msg ++ ": " ++ show (e :: IOError)
logError combinedmsg
return . Bad $ combinedmsg)
(return . okfn)
warn :: String -> IO ()
warn = hPutStrLn stderr . (++) "Warning: "
extractKey :: [Either Integer String]
-> String
-> ([Either Integer String], String)
extractKey ek [] = (reverse ek, [])
extractKey ek xs@(x:_) =
let (span_fn, conv_fn) = if isDigit x
then (isDigit, Left . read)
else (not . isDigit, Right)
(k, rest) = span span_fn xs
in extractKey (conv_fn k:ek) rest
niceSort :: [String] -> [String]
niceSort = niceSortKey id
niceSortKey :: (a -> String) -> [a] -> [a]
niceSortKey keyfn =
map snd . sortBy (compare `on` fst) .
map (\s -> (fst . extractKey [] $ keyfn s, s))
rStripSpace :: String -> String
rStripSpace = reverse . dropWhile isSpace . reverse
newUUID :: IO String
newUUID = do
contents <- readFile ConstantUtils.randomUuidFile
return $! rStripSpace $ take 128 contents
getCurrentTime :: IO Integer
getCurrentTime = do
TOD ctime _ <- getClockTime
return ctime
getCurrentTimeUSec :: IO Integer
getCurrentTimeUSec = do
TOD ctime pico <- getClockTime
return $ ctime * 1000000 + pico `div` 1000000
clockTimeToString :: ClockTime -> String
clockTimeToString (TOD t _) = show t
chompPrefix :: String -> String -> Maybe String
chompPrefix pfx str =
if pfx `isPrefixOf` str || str == init pfx
then Just $ drop (length pfx) str
else Nothing
wrap :: Int
-> String
-> [String]
wrap maxWidth = filter (not . null) . map trim . wrap0
where wrap0 :: String -> [String]
wrap0 text
| length text <= maxWidth = [text]
| isSplitOK = line : wrap0 rest
| otherwise = line' : wrap0 rest'
where (line, rest) = splitAt maxWidth text
(revExtra, revLine) = break isSpace . reverse $ line
(line', rest') = (reverse revLine, reverse revExtra ++ rest)
isSplitOK =
null revLine || null revExtra || startsWithSpace rest
startsWithSpace (x:_) = isSpace x
startsWithSpace _ = False
trim :: String -> String
trim = reverse . dropWhile isSpace . reverse . dropWhile isSpace
defaultHead :: a -> [a] -> a
defaultHead def [] = def
defaultHead _ (x:_) = x
exitIfEmpty :: String -> [a] -> IO a
exitIfEmpty _ (x:_) = return x
exitIfEmpty s [] = exitErr s
monadicThe :: (Eq a, Monad m) => String -> [a] -> m a
monadicThe s [] = fail s
monadicThe s (x:xs)
| all (x ==) xs = return x
| otherwise = fail s
splitEithers :: [Either a b] -> ([a], [b], [Bool])
splitEithers = foldl' splitter ([], [], [])
where splitter (l, r, t) e =
case e of
Left v -> (v:l, r, False:t)
Right v -> (l, v:r, True:t)
recombineEithers :: (Show a, Show b) =>
[a] -> [b] -> [Bool] -> Result [Either a b]
recombineEithers lefts rights trail =
foldM recombiner ([], lefts, rights) trail >>= checker
where checker (eithers, [], []) = Ok eithers
checker (_, lefts', rights') =
Bad $ "Inconsistent results after recombination, l'=" ++
show lefts' ++ ", r'=" ++ show rights'
recombiner (es, l:ls, rs) False = Ok (Left l:es, ls, rs)
recombiner (es, ls, r:rs) True = Ok (Right r:es, ls, rs)
recombiner (_, ls, rs) t = Bad $ "Inconsistent trail log: l=" ++
show ls ++ ", r=" ++ show rs ++ ",t=" ++
show t
resolveAddrHints :: Maybe AddrInfo
resolveAddrHints =
Just defaultHints { addrFlags = [AI_NUMERICHOST, AI_NUMERICSERV] }
resolveAddr :: Int -> String -> IO (Result (Family, SockAddr))
resolveAddr port str = do
resolved <- getAddrInfo resolveAddrHints (Just str) (Just (show port))
return $ case resolved of
[] -> Bad "Invalid results from lookup?"
best:_ -> Ok (addrFamily best, addrAddress best)
setOwnerAndGroupFromNames :: FilePath -> GanetiDaemon -> GanetiGroup -> IO ()
setOwnerAndGroupFromNames filename daemon dGroup = do
runtimeEnts <- getEnts
ents <- exitIfBad "Can't find required user/groups" runtimeEnts
let uid = fst ents M.! daemon
let gid = snd ents M.! dGroup
setOwnerAndGroup filename uid gid
formatOrdinal :: (Integral a, Show a) => a -> String
formatOrdinal num
| num > 10 && num < 20 = suffix "th"
| tens == 1 = suffix "st"
| tens == 2 = suffix "nd"
| tens == 3 = suffix "rd"
| otherwise = suffix "th"
where tens = num `mod` 10
suffix s = show num ++ s
atomicWriteFile :: FilePath -> String -> IO ()
atomicWriteFile path contents = do
(tmppath, tmphandle) <- openTempFile (takeDirectory path) (takeBaseName path)
hPutStr tmphandle contents
hClose tmphandle
renameFile tmppath path
lockFile :: FilePath -> IO (Result ())
lockFile path = do
handle <- openFile path WriteMode
fd <- handleToFd handle
Control.Monad.liftM (either (Bad . show) Ok)
(try (setLock fd (WriteLock, AbsoluteSeek, 0, 0)) :: IO (Either IOError ()))
type FStat = (EpochTime, FileID, FileOffset)
nullFStat :: FStat
nullFStat = (1, 1, 1)
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)
getFStatSafe :: FilePath -> IO FStat
getFStatSafe fpath = liftM (either (const nullFStat) id)
((try $ getFStat fpath) :: IO (Either IOError FStat))
needsReload :: FStat -> FilePath -> IO (Maybe FStat)
needsReload oldstat path = do
newstat <- getFStat path
return $ if newstat /= oldstat
then Just newstat
else Nothing
watchFileEx :: (Eq a, Eq b) => Integer -> b -> IORef b -> a -> IO a -> IO a
watchFileEx endtime base ref old read_fn = do
current <- getCurrentTimeUSec
if current > endtime then read_fn else do
val <- readIORef ref
if val /= base
then do
new <- read_fn
if new /= old then return new else do
logDebug "Observed change not relevant"
threadDelay 100000
watchFileEx endtime val ref old read_fn
else do
threadDelay 100000
watchFileEx endtime base ref old read_fn
watchFile :: Eq a => FilePath -> Int -> a -> IO a -> IO a
watchFile fpath timeout old read_fn = do
current <- getCurrentTimeUSec
let endtime = current + fromIntegral timeout * 1000000
fstat <- getFStatSafe fpath
ref <- newIORef fstat
inotify <- initINotify
let do_watch e = do
logDebug $ "Notified of change in " ++ fpath
++ "; event: " ++ show e
when (e == Ignored)
(addWatch inotify [Modify, Delete] fpath do_watch
>> return ())
fstat' <- getFStatSafe fpath
writeIORef ref fstat'
_ <- addWatch inotify [Modify, Delete] fpath do_watch
newval <- read_fn
if newval /= old
then do
logDebug $ "File " ++ fpath ++ " changed during setup of inotify"
killINotify inotify
return newval
else do
result <- watchFileEx endtime fstat ref old read_fn
killINotify inotify
return result
data FilePermissions = FilePermissions { fpOwner :: Maybe String
, fpGroup :: Maybe String
, fpPermissions :: FileMode
}
ensurePermissions :: FilePath -> FilePermissions -> IO (Result ())
ensurePermissions fpath perms = do
eitherFileStatus <- try $ getFileStatus fpath
:: IO (Either IOError FileStatus)
(flip $ either (return . Bad . show)) eitherFileStatus $ \fstat -> do
ownertry <- case fpOwner perms of
Nothing -> return $ Right ()
Just owner -> try $ do
ownerid <- userID `liftM` getUserEntryForName owner
unless (ownerid == fileOwner fstat) $ do
logDebug $ "Changing owner of " ++ fpath ++ " to " ++ owner
setOwnerAndGroup fpath ownerid (1)
grouptry <- case fpGroup perms of
Nothing -> return $ Right ()
Just grp -> try $ do
groupid <- groupID `liftM` getGroupEntryForName grp
unless (groupid == fileGroup fstat) $ do
logDebug $ "Changing group of " ++ fpath ++ " to " ++ grp
setOwnerAndGroup fpath (1) groupid
let fp = fpPermissions perms
permtry <- if fileMode fstat == fp
then return $ Right ()
else try $ do
logInfo $ "Changing permissions of " ++ fpath ++ " to "
++ showOct fp ""
setFileMode fpath fp
let errors = E.lefts ([ownertry, grouptry, permtry] :: [Either IOError ()])
if null errors
then return $ Ok ()
else return . Bad $ show errors
safeRenameFile :: FilePermissions -> FilePath -> FilePath -> IO (Result ())
safeRenameFile perms from to = do
directtry <- try $ renameFile from to
case (directtry :: Either IOError ()) of
Right () -> return $ Ok ()
Left _ -> do
result <- try $ do
let dir = takeDirectory to
createDirectoryIfMissing True dir
_ <- ensurePermissions dir perms
renameFile from to
return $ either (Bad . show) Ok (result :: Either IOError ())