batchd-0.1.1.0: src/Batchd/Client/Actions.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE DeriveGeneric #-}
-- | This module contains definitions of client actions.
-- These functions prepare REST request structure, execute HTTP request
-- and show response in human-readable form to stdout.
module Batchd.Client.Actions where
import Prelude hiding ((<>))
import Control.Monad
import Control.Monad.State
import Data.Int
import Data.Maybe
import Data.Time.Clock
import Data.Time.LocalTime
import qualified Data.Map as M
import qualified Data.Text as T
import qualified Data.Text.Lazy as TL
import qualified Data.Text.IO as TIO
import qualified Data.Text.Lazy.IO as TLIO
import Data.Text.Format.Heavy
import Data.Char
import Data.List (intercalate, transpose)
import Data.Aeson
import System.FilePath
import System.Exit
import Text.Printf
import Text.PrettyPrint.Boxes
import Batchd.Core.Common.Localize
import qualified Batchd.Common.Data as Database
import Batchd.Common.Types
import Batchd.Common.Schedule
import Batchd.Common.Config
import Batchd.Client.CmdLine
import Batchd.Client.Config
import Batchd.Client.Http
import Batchd.Client.Monad
localTimeToUTC' :: Maybe (Maybe LocalTime) -> Client (Maybe (Maybe UTCTime))
localTimeToUTC' Nothing = return Nothing
localTimeToUTC' (Just Nothing) = return (Just Nothing)
localTimeToUTC' (Just (Just local)) = do
tz <- liftIO $ getCurrentTimeZone
return $ Just $ Just $ localTimeToUTC tz local
-- | Put job to the queue.
doEnqueue :: Client ()
doEnqueue = do
cfg <- gets csConfig
baseUrl <- getBaseUrl
creds <- getCredentials
opts <- gets csCmdline
t <- liftIO $ getTypeName opts cfg
jtr <- liftIO $ loadTemplate (T.pack t)
case jtr of
Left err -> throwC =<< (__f "Can't load job type description: {}" (Single $ Shown err))
Right jtype -> do
qname <- liftIO $ getQueueName opts cfg
host <- liftIO $ getHostName opts cfg
jobStartTime <- join `fmap` localTimeToUTC' (startTime $ cmdCommand opts)
let job = JobInfo {
jiId = 0,
jiQueue = qname,
jiType = t,
jiSeq = 0,
jiUserName = fst creds,
jiCreateTime = zeroUtcTime,
jiStartTime = jobStartTime,
jiStatus = New,
jiTryCount = 0,
jiHostName = host,
jiNotes = jobNotes (cmdCommand opts),
jiResultTime = Nothing,
jiExitCode = Nothing,
jiStdout = Nothing,
jiStderr = Nothing,
jiParams = parseParams (jtParams jtype) (cmdCommand opts)
}
-- debug (__ "Job to be queued: {}") (Single $ show job)
let url = baseUrl </> "queue" </> qname
doPost url job
mkTable :: [[String]] -> Box
mkTable table = hsep 1 top [vcat left (map text column) | column <- table]
printTable :: Int -> [[String]] -> IO ()
printTable indent table =
printBox $ emptyBox 0 indent <> mkTable table
-- | List queues or jobs.
doList :: Client ()
doList = do
baseUrl <- getBaseUrl
opts <- gets csCmdline
let command = cmdCommand opts
case queueToList command of
[] -> do
let url = baseUrl </> "queue"
response <- doGet url
let queuesTable = flip map (response :: [Database.Queue]) $ \queue ->
[(if Database.queueEnabled queue then "[*]" else "[ ]") :: String,
Database.queueName queue,
Database.queueTitle queue,
Database.queueScheduleName queue,
fromMaybe "*" $ Database.queueHostName queue,
maybe "no" show $ Database.queueAutostartJobCount queue]
liftIO $ printTable 0 $ transpose queuesTable
qnames ->
forM_ qnames $ \qname -> do
statusOpt <- parseStatus (Just New) (throwC =<< (__ "Invalid status")) (status command)
let statusStr = case statusOpt of
Nothing -> "?status=all"
Just st -> case status command of
Nothing -> ""
_ -> "?status=" ++ map toLower (show st)
let url = baseUrl </> "queue" </> qname </> "jobs" ++ statusStr
response <- doGet url
liftIO $ forM_ (response :: [JobInfo]) $ \job -> do
printf "#%d: [%d]\t%s\t%s\t%s\n" (jiId job) (jiSeq job) (jiType job) (show $ jiStatus job) (fromMaybe "" $ jiNotes job)
forM_ (M.assocs $ jiParams job) $ \(name, value) -> do
printf "\t%s:\t%s\n" name value
-- | Retrieve statistics
doStats :: Client ()
doStats = do
baseUrl <- getBaseUrl
opts <- gets csCmdline
let command = cmdCommand opts
case queueToStat command of
[] -> do
let url = baseUrl </> "stats"
response <- doGet url
liftIO $ forM_ (M.assocs response) $ \record -> do
let qname = fst record :: String
stats = snd record
liftIO $ putStrLn $ qname ++ ":"
liftIO $ printStats stats
qnames -> do
forM_ qnames $ \qname -> do
liftIO $ putStrLn $ qname ++ ":"
let url = baseUrl </> "stats" </> qname
response <- doGet url
liftIO $ printStats response
where
printStats :: ByStatus Int -> IO ()
printStats (ByStatus stat) = do
printTable 4 $ transpose $ [[show status ++ ":", show count] | (status, count) <- M.assocs stat]
printField :: Formatable value => TL.Text -> IO TL.Text -> value -> IO ()
printField sep ioName value = do
name <- ioName
TLIO.putStrLn $ format "{}{}:\t{}" (sep, name, value)
translateTable :: [(IO TL.Text, String)] -> IO [[String]]
translateTable pairs =
forM pairs $ \(ioTitle, value) -> do
title <- ioTitle
return [TL.unpack title ++ ":", value]
translateTableT :: [(IO TL.Text, T.Text)] -> IO [[T.Text]]
translateTableT pairs =
forM pairs $ \(ioTitle, value) -> do
title <- ioTitle
return [TL.toStrict title `T.append` ":", value]
-- | View job details or results.
viewJob :: Client ()
viewJob = do
baseUrl <- getBaseUrl
opts <- gets csCmdline
let command = cmdCommand opts
let showDescription = if viewDescription command
then True
else if not (viewResult command) && not (viewAll command)
then True
else viewDescription command
if (showDescription || viewResult command) && not (viewAll command)
then do
let url = baseUrl </> "job" </> show (jobId command)
response <- doGet url
when (showDescription) $
liftIO $ printJob response
when (viewResult command) $
liftIO $ printLastResult response
else do
when (showDescription) $ do
let url = baseUrl </> "job" </> show (jobId command)
response <- doGet url
liftIO $ printJob response
when (viewAll command) $ do
let url = baseUrl </> "job" </> show (jobId command) </> "results"
response <- doGet url
let showEc Nothing = "-"
showEc (Just ExitSuccess) = "0"
showEc (Just (ExitFailure n)) = show n
let responseTable = flip map (response :: [Database.JobResult]) $ \result ->
[show (Database.jobResultTime result),
showEc (Database.jobResultExitCode result),
T.unpack (Database.jobResultStdout result)
]
liftIO $ printTable 0 $ transpose responseTable
where
printJob :: JobInfo -> IO ()
printJob job = do
let host = fromMaybe "*" $ jiHostName job
table <- translateTable $ [
((__ "Order"), show $ jiSeq job),
((__ "Type"), jiType job),
((__ "Queue"), jiQueue job),
((__ "Host"), host),
((__ "Notes"), fromMaybe "-" $ jiNotes job),
((__ "User"), jiUserName job),
((__ "Created"), show $ jiCreateTime job),
((__ "Start"), maybe "*" show $ jiStartTime job),
((__ "Status"), show $ jiStatus job),
((__ "Try count"), show $ jiTryCount job)
]
printTable 0 $ transpose table
let params = [[TL.unpack name ++ ":", T.unpack value] | (name, value) <- M.assocs (jiParams job)]
printTable 4 $ transpose params
printLastResult :: JobInfo -> IO ()
printLastResult job = do
let time = case jiResultTime job of
Nothing -> "-"
Just t -> show t
let code = show (jiExitCode job)
TLIO.putStrLn =<< (__f "Exit code:\t{}\nTime:\t{}\n\n" (code, time))
case jiStdout job of
Nothing -> return ()
Just text -> putStrLn $ T.unpack text
printResult :: Database.JobResult -> IO ()
printResult r = do
let time = show (Database.jobResultTime r)
let code = show (Database.jobResultExitCode r)
TLIO.putStrLn =<< (__f "Exit code:\t{}\nTime:\t{}\n\n" (code, time))
putStrLn $ T.unpack $ Database.jobResultStdout r
-- checkIncompatibleOptions :: [(String, Bool)] -> Client ()
-- checkIncompatibleOptions opts =
-- when (length (filter snd opts) > 1) $ do
-- throwC $ printf "Only one option of %s can be specified at once" enabled
-- where
-- enabled = intercalate ", " $ map fst $ filter snd opts
checkModes :: [Client (Maybe a)] -> Client a
checkModes list = go Nothing list
where
go Nothing [] = throwC =<< (__ "No mode is selected")
go (Just m) [] = return m
go Nothing (m:ms) = do
result <- m
go result ms
go selected@(Just _) (m:ms) = do
result <- m
case result of
Nothing -> go selected ms
Just _ -> throwC =<< (__ "Only one mode can be selected")
-- | Update job.
updateJob :: Client ()
updateJob = do
baseUrl <- getBaseUrl
opts <- gets csCmdline
let command = cmdCommand opts
job <- checkModes [mMove command, mPrioritize command, mUpdate command]
let url = baseUrl </> "job" </> show (jobId command)
doPut url job
where
mMove command = do
case queueName command of
Just qname -> return $ Just $ object ["move" .= qname]
Nothing -> return Nothing
mPrioritize command = do
case prioritize command of
Just action -> return $ Just $ object ["priority" .= action]
Nothing -> return Nothing
mUpdate command = do
if isNothing (queueName command) && isNothing (prioritize command)
then do
jobStartTime <- localTimeToUTC' (startTime command)
return $ Just $ object $
toList "status" (status command) ++
toList "host_name" (hostName command) ++
toList "notes" (jobNotes command) ++
toList "start_time" jobStartTime
else return Nothing
-- | Delete job.
deleteJob :: Client ()
deleteJob = do
baseUrl <- getBaseUrl
opts <- gets csCmdline
let command = cmdCommand opts
let url = baseUrl </> "job" </> show (jobId command)
doDelete url
-- | Create queue.
addQueue :: Client ()
addQueue = do
baseUrl <- getBaseUrl
opts <- gets csCmdline
let command = cmdCommand opts
let queue = Database.Queue {
Database.queueName = queueObject command,
Database.queueTitle = fromMaybe (queueObject command) (title command),
Database.queueEnabled = fromMaybe True (enabled command),
Database.queueScheduleName = fromMaybe "anytime" (scheduleName command),
Database.queueHostName = hostName command,
Database.queueAutostartJobCount = join (autostartCount command)
}
let url = baseUrl </> "queue"
doPost url queue
-- | Update queue.
updateQueue :: Client ()
updateQueue = do
baseUrl <- getBaseUrl
opts <- gets csCmdline
let command = cmdCommand opts
let queue = object $
toList "enabled" (enabled command) ++
toList "title" (title command) ++
toList "schedule_name" (scheduleName command) ++
toList "host_name" (hostName command) ++
toList "autostart_job_count" (autostartCount command)
-- print queue
let url = baseUrl </> "queue" </> queueObject command
doPut url queue
toList :: (ToJSON v, KeyValue t) => T.Text -> Maybe v -> [t]
toList _ Nothing = []
toList name (Just str) = [name .= str]
-- | Delete queue.
deleteQueue :: Client ()
deleteQueue = do
baseUrl <- getBaseUrl
opts <- gets csCmdline
let command = cmdCommand opts
let forceStr = if force command then "?forced=true" else ""
let url = baseUrl </> "queue" </> queueObject command ++ forceStr
doDelete url
-- | List schedules
doListSchedules :: Client ()
doListSchedules = do
baseUrl <- getBaseUrl
opts <- gets csCmdline
let command = cmdCommand opts
let url = baseUrl </> "schedule"
response <- doGet url
let check = if null (scheduleNames command)
then const True
else \si -> sName si `elem` scheduleNames command
liftIO $ forM_ (response :: [ScheduleInfo]) $ \si -> do
when (check si) $ do
putStrLn $ sName si ++ ":"
case sWeekdays si of
Nothing -> putStrLn "\tany weekday"
Just lst -> putStrLn $ "\t" ++ intercalate ", " (map show lst)
case sTime si of
Nothing -> putStrLn "\tany time of day"
Just lst -> putStrLn $ "\t" ++ intercalate ", " (map show lst)
-- | Create a schedule.
doAddSchedule :: Client ()
doAddSchedule = do
baseUrl <- getBaseUrl
opts <- gets csCmdline
let command = cmdCommand opts
when (length (scheduleNames command) /= 1) $
throwC =<< (__ "Exactly one schedule name must be specified when creating a schedule")
let ts = forM (periods command) $ \str -> do
parsePeriod str
case ts of
Left err -> throwC =<< (__f "Can't parse period description: {}" (Single $ show err))
Right times -> do
let url = baseUrl </> "schedule"
let schedule = ScheduleInfo {
sName = head (scheduleNames command),
sWeekdays = if null (weekdays command) then Nothing else Just (weekdays command),
sTime = if null times then Nothing else Just times
}
doPost url schedule
-- | Delete schedule.
doDeleteSchedule :: Client ()
doDeleteSchedule = do
baseUrl <- getBaseUrl
opts <- gets csCmdline
let command = cmdCommand opts
when (length (scheduleNames command) /= 1) $
throwC =<< (__ "Exactly one schedule name must be specified when deleting a schedule")
let sname = head (scheduleNames command)
let forceStr = if force command then "?forced=true" else ""
let url = baseUrl </> "schedule" </> sname ++ forceStr
doDelete url
-- | List job types.
doType :: Client ()
doType = do
baseUrl <- getBaseUrl
opts <- gets csCmdline
let command = cmdCommand opts
let url = baseUrl </> "type"
response <- doGet url
let check = if null (types command)
then const True
else \jt -> jtName jt `elem` types command
liftIO $ forM_ (response :: [JobType]) $ \jt -> do
when (check jt) $ do
let title = fromMaybe (jtName jt) (jtTitle jt)
putStrLn $ jtName jt ++ ":"
table <- translateTable $
[(__ "Title", title)] ++
translateTemplate (jtTemplate jt) ++
[(__ "On fail", show (jtOnFail jt)),
(__ "Host", fromMaybe "*" (jtHostName jt))]
printTable 4 $ transpose table
paramsLine <- (__ "Parameters:")
TLIO.putStrLn $ " " `TL.append` paramsLine
forM_ (jtParams jt) $ \desc -> do
paramsTable <- translateTable $ [
(__ "Name", TL.unpack $ piName desc),
(__ "Type", show (piType desc)),
(__ "Title", TL.unpack $ piTitle desc),
(__ "Default", T.unpack $ piDefault desc)
]
let box = emptyBox 0 4 <> char '*' <+> mkTable (transpose paramsTable)
printBox box
where
translateTemplate (line : lines) =
(__ "Template", T.unpack line) :
[(return "", T.unpack l) | l <- lines]
-- | List defined hosts.
doHosts :: Client ()
doHosts = do
baseUrl <- getBaseUrl
opts <- gets csCmdline
let command = cmdCommand opts
let url = baseUrl </> "host"
response <- doGet url
liftIO $ forM_ (response :: [T.Text]) $ \hostName -> do
TIO.putStrLn hostName
doMonitor :: Client ()
doMonitor = do
baseUrl <- getBaseUrl
opts <- gets csCmdline
let command = cmdCommand opts
let monitor = baseUrl </> "monitor"
url <- case metricTime command of
LastSample -> case metricPrefix command of
Nothing -> fail "metric name must be specified for --last mode"
Just name -> return $ monitor </> name </> "last"
CurrentSample ->
let byPrefix = case metricPrefix command of
Nothing -> monitor </> "current"
Just prefix -> monitor </> prefix </> "current"
in return $ case metricView command of
Plain -> byPrefix </> "plain"
Tree -> byPrefix </> "tree"
response <- doGet url
-- liftIO $ putStrLn $ show (response :: Value)
liftIO $ forM_ (response :: [Database.MetricRecord]) $ \r -> do
TIO.putStr $ Database.metricRecordName r `T.append` ": "
case Database.metricRecordKind r of
Counter -> TLIO.putStrLn =<< (__f "count: {value}" r)
Gauge -> TLIO.putStrLn =<< (__f "value: {value}" r)
Label -> TLIO.putStrLn =<< (__f "text: {text}" r)
Distribution -> do
TLIO.putStrLn =<< (__f "mean: {mean}; variance: {variance}; count: {count}; sum: {sum}; min: {min}; max: {max}" r)
-- | List users.
doListUsers :: Client ()
doListUsers = do
baseUrl <- getBaseUrl
opts <- gets csCmdline
let url = baseUrl </> "user"
response <- doGet url
liftIO $ forM_ (response :: [String]) $ \name -> putStrLn name
-- | Create user
doAddUser :: Client ()
doAddUser = do
baseUrl <- getBaseUrl
opts <- gets csCmdline
let command = cmdCommand opts
let url = baseUrl </> "user"
pwd <- liftIO $ getPassword2
name <- case objectUserName command of
[n] -> return n
_ -> fail "user name must be provided"
let user = UserInfo name pwd
doPost url user
-- | Change user password
doChangePassword :: Client ()
doChangePassword = do
baseUrl <- getBaseUrl
opts <- gets csCmdline
let command = cmdCommand opts
creds <- getCredentials
name <- case objectUserName command of
[n] -> return n
_ -> return $ fst creds
let url = baseUrl </> "user" </> name
pwd <- liftIO $ getPassword2
let user = UserInfo name pwd
doPut url user
-- | List user permissions
doListPermissions :: Client ()
doListPermissions = do
baseUrl <- getBaseUrl
opts <- gets csCmdline
let command = cmdCommand opts
let url = baseUrl </> "user" </> grantUserName command </> "permissions"
response <- doGet url
liftIO $ forM_ (response :: [(Int64, Database.UserPermission)]) $ \(id, perm) -> do
table <- translateTable $ [
(__ "ID", show id),
(__ "Permission", show $ Database.userPermissionPermission perm),
(__ "Queue", fromMaybe "*" $ Database.userPermissionQueueName perm),
(__ "Job type", fromMaybe "*" $ Database.userPermissionTypeName perm),
(__ "Host", T.unpack $ fromMaybe "*" $ Database.userPermissionHostName perm)
]
printTable 0 $ transpose table
putStrLn ""
-- | Add user permission
doAddPermission :: Client ()
doAddPermission = do
baseUrl <- getBaseUrl
opts <- gets csCmdline
let command = cmdCommand opts
let name = grantUserName command
url = baseUrl </> "user" </> name </> "permissions"
perm <- case permission command of
Just p -> return $ Database.UserPermission name p (queueName command) (typeName command) (T.pack <$> hostName command)
Nothing -> throwC =<< (__ "permission (-p) must be specified")
doPost url perm
-- | Revoke user permission.
doRevokePermission :: Client ()
doRevokePermission = do
baseUrl <- getBaseUrl
opts <- gets csCmdline
let command = cmdCommand opts
let name = grantUserName command
url <- case grantPermissionId command of
Just permId -> return $ baseUrl </> "user" </> name </> "permissions" </> show permId
Nothing -> throwC =<< (__ "permission ID (-i) must be specified")
-- liftIO $ print url
doDelete url