module Ganeti.Utils
( debug
, debugFn
, debugXy
, sepSplit
, stdDev
, if'
, select
, applyIf
, commaJoin
, ensureQuoted
, tryRead
, formatTable
, printTable
, parseUnit
, plural
, niceSort
, niceSortKey
, exitIfBad
, exitErr
, exitWhen
, exitUnless
, logWarningIfBad
, rStripSpace
, newUUID
, getCurrentTime
, getCurrentTimeUSec
, clockTimeToString
, chompPrefix
, warn
, wrap
, trim
, defaultHead
, exitIfEmpty
, splitEithers
, recombineEithers
, setOwnerAndGroupFromNames
) where
import Data.Char (toUpper, isAlphaNum, isDigit, isSpace)
import Data.Function (on)
import Data.List
import qualified Data.Map as M
import Control.Monad (foldM)
import Debug.Trace
import Ganeti.BasicTypes
import qualified Ganeti.Constants as C
import Ganeti.Logging
import Ganeti.Runtime
import System.IO
import System.Exit
import System.Posix.Files
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)
if' :: Bool
-> a
-> a
-> a
if' True x _ = x
if' _ _ y = y
parseChoices :: (Monad m, Read a) => 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) => String -> m Rational
parseUnitValue 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 = 1000 :: Rational
decToBin = kbDecimal / kbBinary
mbFactor = decToBin * decToBin
parseUnit :: (Monad m, Integral a, Read a) => String -> m a
parseUnit str =
case (reads str::[(Int, String)]) of
[(v, suffix)] ->
let unit = dropWhile (== ' ') suffix
in do
scaling <- parseUnitValue unit
return $ truncate (fromIntegral v * scaling)
_ -> fail $ "Can't parse string '" ++ str ++ "'"
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
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 C.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
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
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