packages feed

batchd-0.1.1.0: src/Batchd/Client/CmdLine.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE DeriveGeneric #-}
-- | This module contains definitions for client command-line parsing
-- by using optparse-applicative package.
module Batchd.Client.CmdLine where

import Data.Generics hiding (Generic)
import qualified Data.Map as M
import Data.Int
import Data.Time.LocalTime
import Data.Dates
import Data.Char (toLower)
import qualified Data.Text as T
import qualified Data.Text.Lazy as TL
import Options.Applicative
import System.Log.Heavy
import qualified Text.Parsec as Parsec 

import System.IO.Unsafe (unsafePerformIO)

import Batchd.Core.Common.Types
import Batchd.Common.Types
import Batchd.Common.Data (MoveAction (..))
import Batchd.Client.Types

data CmdLine = CmdLine {
    cmdCommon :: CommonOpts,
    cmdCommand :: Command
  } deriving (Eq, Show)

data CommonOpts = CommonOpts {
      managerUrl :: Maybe String,
      username :: Maybe String,
      password :: Maybe String,
      logLevel :: Level
    }
  deriving (Eq, Show)
    

data Command =
    Enqueue {
      queueName :: Maybe String,
      typeName :: Maybe String,
      hostName :: Maybe String,
      startTime :: Maybe (Maybe LocalTime),
      jobNotes :: Maybe String,
      jobCommand :: [T.Text],
      parameters :: [String]
    }
  | List {
      status :: Maybe String,
      queueToList :: [String]
    }
  | Queue {
      queueMode :: CrudMode,
      queueObject :: String,
      scheduleName :: Maybe String,
      hostName :: Maybe String,
      title :: Maybe String,
      enabled :: Maybe Bool,
      autostartCount :: Maybe (Maybe Int),
      force :: Bool
    }
  | Job {
      jobId :: Int,
      queueName :: Maybe String,
      prioritize :: Maybe MoveAction,
      startTime :: Maybe (Maybe LocalTime),
      status :: Maybe String,
      hostName :: Maybe String,
      jobNotes :: Maybe String,
      viewDescription :: Bool,
      viewResult :: Bool,
      viewAll :: Bool,
      jobMode :: CrudMode
    }
  | Schedule {
      scheduleMode :: CrudMode,
      scheduleNames :: [String],
      periods :: [String],
      weekdays :: [WeekDay],
      force :: Bool
    }
  | Monitor {
      metricPrefix :: Maybe String,
      metricView :: MetricView,
      metricTime :: MetricTime
    }
  | User {
      userMode :: CrudMode,
      objectUserName :: [String]
    }
  | Grant {
      grantMode :: CrudMode,
      grantUserName :: String,
      grantPermissionId :: Maybe Int64,
      permission :: Maybe Permission,
      queueName :: Maybe String,
      typeName :: Maybe String,
      hostName :: Maybe String
    }
  | Type {
      types :: [String]
    }
  | Stats {
      queueToStat :: [String]
    }
  | Hosts
  | RunShell
  deriving (Eq, Show, Data, Typeable)

data MetricTime = CurrentSample | LastSample
  deriving (Eq, Show, Data, Typeable)

data MetricView = Tree | Plain
  deriving (Eq, Show, Data, Typeable)

defaultUrl :: String
defaultUrl = "http://localhost:" ++ show defaultManagerPort

defaultQueue :: String
defaultQueue = "default"

defaultType :: String
defaultType = "command"

-- managerUrlAnn = Nothing &= name "url" &= typ defaultUrl &= help "batchd manager API URL"

-- usernameAnn = Nothing &= name "user" &= typ "USER" &= help "batchd user name"

-- passwordAnn = Nothing &= name "password" &= typ "PASSWORD" &= help "batchd user password"

parser :: Parser CmdLine
parser = CmdLine <$> commonOpts <*> commands

commonOpts :: Parser CommonOpts
commonOpts = CommonOpts
    <$> optional (strOption
        ( long "manager-url"
        <> short 'M'
        <> metavar "URL"
        <> help "batchd manager API URL"))
    <*> optional (strOption
        ( long "username"
        <> short 'U'
        <> metavar "NAME"
        <> help "batchd user name"))
    <*> optional (strOption
        ( long "password"
        <> short 'P'
        <> metavar "PASSWORD"
        <> help "batchd user password"))
    <*> verbosity

verbosity :: Parser Level
verbosity =
      flag' debug_level (short 'd' <> long "debug" <> help "enable client debug output")
  <|> flag' verbose_level (short 'v' <> long "verbose" <> help "be verbose")
  <|> flag info_level disable_logging (short 'q' <> long "quiet" <> help "be quiet")

pMetricView :: Parser MetricView
pMetricView =
      flag Plain Tree (short 't' <> long "tree" <> help "output data in tree view")

pMetricTime :: Parser MetricTime
pMetricTime =
      flag CurrentSample LastSample (short 'l' <> long "last" <> help "retrieve last metrics sample from database")

required :: Read a => String -> Char -> String -> String -> Parser a
required longName shortName meta helpText =
  option auto (long longName <> short shortName <> metavar meta <> help helpText)

optionalF :: Read a => String -> Char -> String -> String -> Parser (Maybe a)
optionalF longName shortName meta helpText =
  optional $ required longName shortName meta helpText

requiredString :: String -> Char -> String -> String -> Parser String
requiredString longName shortName meta helpText =
  strOption (long longName <> short shortName <> metavar meta <> help helpText)

optionalString :: String -> Char -> String -> String -> Parser (Maybe String)
optionalString longName shortName meta helpText =
  optional $ requiredString longName shortName meta helpText

enqueue :: Parser Command
enqueue = Enqueue
  <$> optionalString "queue" 'q' "QUEUE" "queue name"
  <*> optionalString "type"  't' "TYPE"  "job type name"
  <*> optionalString "host"  'h' "HOST"  "worker host name"
  <*> (optional $ option timeReader (long "at" <> long "start" <> metavar "YYYY-MM-DD HH:MM:SS" <> help "set job start time"))
  <*> optionalString "notes" 'n' "TEXT" "job description"
  <*> many (strArgument  (metavar "COMMAND"))
  <*> many (requiredString "parameter" 'p' "NAME=VALUE" "job parameters specified by name")

list :: Parser Command
list = List
  <$> (optionalString "status" 's' "STATUS" "list only jobs of specified status"
       <|> flag Nothing (Just "all") (long "all" <> short 'a' <> help "synonym for --status=all"))
  <*> many (strArgument (metavar "QUEUE" <> help "queue to list"))

crudMode :: [(CrudMode, String)] -> Parser CrudMode
crudMode [] = error "crudMode called with empty list"
crudMode modes@((dflt,_):_) = foldr1 (<|>) $ map go modes
  where
    go (mode, helpText) =
      let (longName, shortName) = case mode of
                                    View -> ("view", 'v')
                                    Add -> ("add", 'a')
                                    Update -> ("update", 'u')
                                    Delete -> ("delete", 'd')
      in flag dflt mode
          ( long longName 
          <> short shortName
          <> help helpText )

priorityReader :: ReadM MoveAction
priorityReader = maybeReader (check . map toLower)
  where
    check s
      | s `elem` ["first", "f"] = Just First
      | s `elem` ["more", "up", "u"] = Just More
      | s `elem` ["less", "down", "d"] = Just Less
      | s `elem` ["last", "l"] = Just Last
      | otherwise = Nothing

startupTime :: DateTime
startupTime = unsafePerformIO $ getCurrentDateTime
{-# NOINLINE startupTime #-}

timeReader :: ReadM (Maybe LocalTime)
timeReader = (maybeReader nothing) <|> (Just <$> toUtc <$> eitherReader readDateTime)
  where
    toUtc :: DateTime -> LocalTime
    toUtc dt = LocalTime day time
      where
        day = dateTimeToDay dt
        time = TimeOfDay (hour dt) (minute dt) (fromIntegral $ second dt)

    nothing :: String -> Maybe (Maybe LocalTime)
    nothing "any" = Just Nothing
    nothing "now" = Just Nothing
    nothing "anytime" = Just Nothing
    nothing _ = Nothing

    readDateTime :: String -> Either String DateTime
    readDateTime str = case Parsec.runParser (do {t <- pDateTime startupTime; Parsec.eof; return t}) () "<start time>" str of
                         Left err -> Left (show err)
                         Right time -> Right time

queue :: Parser Command
queue = Queue
  <$> crudMode [(Update, "modify queue"), (Add, "create new queue"), (Delete, "delete queue")]
  <*> strArgument (metavar "QUEUE" <> help "queue to operate on")
  <*> optionalString "schedule" 's' "SCHEDULE" "queue schedule name"
  <*> optionalString "host" 'h' "HOST" "default host name for queue"
  <*> optionalString "name" 'n' "TITLE" "set queue title"
  <*> (   flag Nothing (Just True) (long "enable"  <> short 'e' <> help "enable queue")
      <|> flag Nothing (Just False) (long "disable" <> short 'D' <> help "disable queue")
      )
  <*> (optional $ option autostartReader (long "autostart" <> metavar "N"
                                   <> help "automatically enable queue if it has N new jobs. Use `--autostart no' to disable."))
  <*> switch (long "force" <> short 'f' <> help "force non-empty queue deletion")

autostartReader :: ReadM (Maybe Int)
autostartReader = (Just <$> auto) <|> maybeReader disable
  where
    disable :: String -> Maybe (Maybe Int)
    disable "no" = Just Nothing
    disable "disable" = Just Nothing
    disable "false" = Just Nothing
    disable _ = Nothing

job :: Parser Command
job = Job
  <$> argument auto (metavar "ID" <> help "job ID")
  <*> optionalString "move" 'm' "QUEUE" "move job to other queue"
  <*> (optional $ option priorityReader (long "priority" <> short 'p' <> metavar "ACTION" <> help "change job priority. ACTION is one of: up, down, first, last"))
  <*> (optional $ option timeReader (long "at" <> long "start" <> metavar "YYYY-MM-DD HH:MM:SS" <> help "set job start time. Use `any' to specify that job can be run at any time."))
  <*> optionalString "status" 's' "STATUS" "set job status"
  <*> optionalString "host" 'h' "HOST" "set job host"
  <*> optionalString "notes" 'n' "TEXT" "job description"
  <*> switch (long "description" <> help "view job description")
  <*> switch (long "result" <> short 'r' <> help "view job result")
  <*> switch (long "all" <> short 'a' <> help "view all job results")
  <*> crudMode [(View, "view job description or result"), (Update, "modify job"), (Delete, "delete job")]
    
schedule :: Parser Command
schedule = Schedule
  <$> crudMode [(View, "list available schedules"), (Add, "create new schedule"), (Update, "modify schedule"), (Delete, "delete (unused) schedule")]
  <*> many (strArgument (metavar "SCHEDULE" <> help "schedules to operate on"))
  <*> many (requiredString "period" 'p' "HH:MM:SS HH:MM:SS" "time of day period(s)")
  <*> many (required "weekday" 'w' "WEEKDAY" "week day(s)")
  <*> switch (long "force" <> short 'f' <> help "delete also all queues which use this schedule and their jobs")

user :: Parser Command
user = User
  <$> crudMode [(View, "list existing users"), (Add, "create new user"), (Update, "change user password")]
  <*> many (strArgument (metavar "NAME" <> help "name of user to operate on"))

grant :: Parser Command
grant = Grant
  <$> crudMode [(View, "view permissions of user"), (Add, "add permissions to user"), (Delete, "revoke permission from user")]
  <*> strArgument (metavar "NAME" <> help "name of user to operate on")
  <*> optionalF "id" 'i' "ID" "permission ID to operate on. mandatory for revoke operation."
  <*> optionalF "permission" 'p' "PERMISSION" "specify permission, for example ViewJobs"
  <*> optionalString "queue" 'q' "QUEUE" "queue to grant permission to, by default - any"
  <*> optionalString "type" 't' "TYPE" "job type to grant permission to, usable for CreateJobs permission"
  <*> optionalString "host" 'h' "HOST" "name of host to grant permission to, usable for CreateJobs permission. Use `__default__' value to allow default host of queue."

typesList :: Parser Command
typesList = Type
  <$> many (strArgument (metavar "TYPE"))

stats :: Parser Command
stats = Stats
  <$> many (strArgument (metavar "QUEUE"))

monitor :: Parser Command
monitor = Monitor
  <$> (optional $ strArgument (metavar "PREFIX" <> help "prefix of names of metrics to retrieve, for example batchd.wai"))
  <*> pMetricView
  <*> pMetricTime

shell :: Parser Command
shell = pure RunShell

hostsList :: Parser Command
hostsList = pure Hosts

parseParams :: [ParamDesc] -> Command -> JobParamInfo
parseParams desc e = 
    let posNames = map piName desc
        ordered = M.fromList $ zip posNames (jobCommand e)
        byName = M.fromList $ map parseOne (parameters e)
    in  M.union byName ordered
  where
    parseOne :: String -> (TL.Text, T.Text)
    parseOne s = case break (== '=') s of
                   (key, (_:value)) -> (TL.pack key, T.pack value)
                   (key, []) -> (TL.pack key, T.empty)

commands :: Parser Command
commands = hsubparser 
    (  cmd "enqueue" enqueue "put a new job into queue"
    <> cmd "ls" list "list queues or jobs"
    <> cmd "job" job "update or delete jobs"
    <> cmd "queue" queue "create, update or delete queues"
    <> cmd "schedule" schedule "create, update or delete schedules"
    <> cmd "monitor" monitor "retrieve monitoring metrics"
    <> cmd "type" typesList "view job types"
    <> cmd "host" hostsList "view defined hosts"
    <> cmd "stats" stats "print statistics on queue or on all jobs"
    <> cmd "user" user "create, update or delete users"
    <> cmd "grant" grant "create, update or delete user permissions"
    <> cmd "shell" shell "run interactive shell" )
  where
    cmd name func helpText = command name (info func (progDesc helpText))

parserInfo :: ParserInfo CmdLine
parserInfo = info (parser <**> helper)
               (fullDesc
               <> header "batch - the batchd toolset command-line client program"
               <> progDesc "operate on batch jobs, queues, schedules etc.")

getCmdArgs :: IO CmdLine
getCmdArgs = execParser parserInfo