module Ganeti.Utils
( debug
, debugFn
, debugXy
, sepSplit
, findFirst
, stdDev
, if'
, select
, applyIf
, commaJoin
, ensureQuoted
, tryRead
, readMaybe
, formatTable
, printTable
, parseUnit
, parseUnitAssumeBinary
, plural
, niceSort
, niceSortKey
, exitIfBad
, exitErr
, exitWhen
, exitUnless
, logWarningIfBad
, rStripSpace
, newUUID
, isUUID
, getCurrentTime
, getCurrentTimeUSec
, clockTimeToString
, clockTimeToCTime
, clockTimeToUSec
, cTimeToClockTime
, chompPrefix
, warn
, wrap
, trim
, defaultHead
, exitIfEmpty
, splitEithers
, recombineEithers
, resolveAddr
, monadicThe
, setOwnerAndGroupFromNames
, setOwnerWGroupR
, formatOrdinal
, tryAndLogIOError
, withDefaultOnIOError
, lockFile
, FStat
, nullFStat
, getFStat
, getFStatSafe
, needsReload
, watchFile
, watchFileBy
, safeRenameFile
, FilePermissions(..)
, ensurePermissions
, ordNub
, isSubsequenceOf
) where
import Control.Applicative
import Control.Concurrent
import Control.Exception (try, bracket)
import Control.Monad
import Control.Monad.Error
import qualified Data.Attoparsec.ByteString as A
import qualified Data.ByteString.UTF8 as UTF8
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 Data.Maybe (fromMaybe)
import qualified Data.Set as S
import Foreign.C.Types (CTime(..))
import Numeric (showOct)
import System.Directory (renameFile, createDirectoryIfMissing)
import System.FilePath.Posix (takeDirectory)
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.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
findFirst :: (Ord a, Enum a) => a -> S.Set a -> a
findFirst base xs =
case S.splitMember base xs of
(_, False, _) -> base
(_, True, ys) -> fromMaybe (succ base) $
(fmap fst . find (uncurry (<)) . zip [succ base..] . S.toAscList $ ys)
`mplus` fmap (succ . fst) (S.maxView ys)
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)
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
readMaybe :: Read a => String -> Maybe a
readMaybe s = case reads s of
[(a, "")] -> Just a
_ -> Nothing
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)
withDefaultOnIOError :: a -> IO a -> IO a
withDefaultOnIOError a io =
try io >>= either (\ (_ :: IOError) -> return a) return
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
uuidCheckParser :: A.Parser ()
uuidCheckParser = do
let lowerHex = A.satisfy (\c -> (48 <= c && c <= 57) ||
(97 <= c && c <= 102))
hx n = A.count n lowerHex
d = A.word8 45
void $ hx 8 >> d >> hx 4 >> d >> hx 4 >> d >> hx 4 >> d >> hx 12
isUUID :: String -> Bool
isUUID =
isRight . A.parseOnly (uuidCheckParser <* A.endOfInput) . UTF8.fromString
getCurrentTime :: IO Integer
getCurrentTime = do
TOD ctime _ <- getClockTime
return ctime
getCurrentTimeUSec :: IO Integer
getCurrentTimeUSec = liftM clockTimeToUSec getClockTime
clockTimeToString :: ClockTime -> String
clockTimeToString (TOD t _) = show t
clockTimeToCTime :: ClockTime -> EpochTime
clockTimeToCTime (TOD secs _) = fromInteger secs
clockTimeToUSec :: ClockTime -> Integer
clockTimeToUSec (TOD ctime pico) =
ctime * 1000000 + pico `div` 1000000
cTimeToClockTime :: EpochTime -> ClockTime
cTimeToClockTime (CTime timet) = TOD (toInteger timet) 0
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 <- runResultT getEnts
ents <- exitIfBad "Can't find required user/groups" runtimeEnts
let uid = reUserToUid ents M.! daemon
let gid = reGroupToGid ents M.! dGroup
setOwnerAndGroup filename uid gid
setOwnerWGroupR :: FilePath -> IO ()
setOwnerWGroupR path = setFileMode path mode
where mode = foldl unionFileModes nullFileMode
[ownerReadMode, ownerWriteMode, groupReadMode]
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
lockFile :: FilePath -> IO (Result Fd)
lockFile path = runResultT . liftIO $ do
handle <- openFile path WriteMode
fd <- handleToFd handle
setLock fd (WriteLock, AbsoluteSeek, 0, 0)
return fd
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 b) => Integer -> b -> IORef b -> (a -> Bool) -> IO a -> IO a
watchFileEx endtime base ref check 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 check new then return new else do
logDebug "Observed change not relevant"
threadDelay 100000
watchFileEx endtime val ref check read_fn
else do
threadDelay 100000
watchFileEx endtime base ref check read_fn
watchFileBy :: FilePath -> Int -> (a -> Bool) -> IO a -> IO a
watchFileBy fpath timeout check read_fn = do
current <- getCurrentTimeUSec
let endtime = current + fromIntegral timeout * 1000000
fstat <- getFStatSafe fpath
ref <- newIORef fstat
bracket initINotify killINotify $ \inotify -> do
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 check newval
then do
logDebug $ "File " ++ fpath ++ " changed during setup of inotify"
return newval
else watchFileEx endtime fstat ref check read_fn
watchFile :: Eq a => FilePath -> Int -> a -> IO a -> IO a
watchFile fpath timeout old = watchFileBy fpath timeout (/= old)
data FilePermissions = FilePermissions { fpOwner :: Maybe GanetiDaemon
, fpGroup :: Maybe GanetiGroup
, fpPermissions :: FileMode
}
ensurePermissions :: FilePath -> FilePermissions -> IO (Result ())
ensurePermissions fpath perms = do
runtimeEnts <- runResultT getEnts
ents <- exitIfBad "Can't determine user/group ids" runtimeEnts
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
let ownerid = reUserToUid ents M.! owner
unless (ownerid == fileOwner fstat) $ do
logDebug $ "Changing owner of " ++ fpath ++ " to " ++ show owner
setOwnerAndGroup fpath ownerid (1)
grouptry <- case fpGroup perms of
Nothing -> return $ Right ()
Just grp -> try $ do
let groupid = reGroupToGid ents M.! grp
unless (groupid == fileGroup fstat) $ do
logDebug $ "Changing group of " ++ fpath ++ " to " ++ show 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 ())
ordNub :: (Ord a) => [a] -> [a]
ordNub =
let go _ [] = []
go s (x:xs) = if x `S.member` s
then go s xs
else x : go (S.insert x s) xs
in go S.empty
isSubsequenceOf :: (Eq a) => [a] -> [a] -> Bool
isSubsequenceOf [] _ = True
isSubsequenceOf _ [] = False
isSubsequenceOf a@(x:a') (y:b) | x == y = isSubsequenceOf a' b
| otherwise = isSubsequenceOf a b