packages feed

sensu-run (empty) → 0.0.0

raw patch · 5 files changed

+355/−0 lines, 5 filesdep +aesondep +basedep +bytestringsetup-changed

Dependencies added: aeson, base, bytestring, filepath, http-client, http-types, lens, network, optparse-applicative, process, temporary, text, time, vector, wreq

Files

+ CHANGELOG.md view
@@ -0,0 +1,5 @@+# Revision history for sensu-run++## 0.0.0  -- 2017-05-16++* Initial release
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright (c) 2016, Mitsutoshi Aoe++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++    * Redistributions of source code must retain the above copyright+      notice, this list of conditions and the following disclaimer.++    * Redistributions in binary form must reproduce the above+      copyright notice, this list of conditions and the following+      disclaimer in the documentation and/or other materials provided+      with the distribution.++    * Neither the name of Mitsutoshi Aoe nor the names of other+      contributors may be used to endorse or promote products derived+      from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ sensu-run.cabal view
@@ -0,0 +1,40 @@+name: sensu-run+version: 0.0.0+synopsis: A tool to send command execution results to Sensu+description:+  @sensu-run@ is a command line tool to send command execution results to Sensu+  monitoring server.+license: BSD3+license-file: LICENSE+author: Mitsutoshi Aoe+maintainer: maoe@foldr.in+copyright: Copyright (C) 2016-2017 Mitsutoshi Aoe+homepage: https://github.com/maoe/sensu-run#readme+bug-reports: https://github.com/maoe/sensu-run/issues+category: System+build-type: Simple+extra-source-files: CHANGELOG.md+cabal-version: >= 1.10+tested-with: GHC >= 8.0.1 && < 8.1++executable sensu-run+  main-is: sensu-run.hs+  build-depends:+      aeson >= 0.11 && < 1.3+    , base >= 4.9 && < 4.10+    , bytestring >= 0.10 && < 0.11+    , filepath >= 1.4.1 && < 1.5+    , http-client >= 0.5.6 && < 0.6+    , http-types >= 0.9.1 && < 0.10+    , lens >= 4.15 && < 4.16+    , network >= 2.2.3 && < 2.7+    , optparse-applicative >= 0.12 && < 0.14+    , process >= 1.4 && < 1.7+    , temporary >= 1.1 && < 1.3+    , text >= 1.2.2 && < 1.3+    , time >= 1.5.0.1 && < 1.9+    , vector >= 0.11 && < 0.13+    , wreq >= 0.5.0 && < 0.6++  ghc-options: -Wall -threaded+  default-language: Haskell2010
+ sensu-run.hs view
@@ -0,0 +1,278 @@+{-# LANGUAGE ApplicativeDo #-}+{-# LANGUAGE DuplicateRecordFields #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE MultiWayIf #-}+{-# LANGUAGE NamedFieldPuns #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE PatternSynonyms #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE StrictData #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE ViewPatterns #-}+module Main where+import Control.Exception+import Data.Foldable+import Data.List.NonEmpty (NonEmpty)+import Data.Maybe+import System.Exit+import System.IO+import qualified Data.List.NonEmpty as NE+import qualified System.Timeout as Timeout++import Control.Lens hiding ((.=))+import Data.Aeson+import Data.Time+import Data.Time.Clock.POSIX+import Network.HTTP.Client (HttpException)+import Network.Socket+import System.FilePath ((</>))+import System.IO.Temp+import System.Process+import qualified Data.ByteString.Lazy as BL+import qualified Data.ByteString.Lazy.Char8 as BL8+import qualified Data.Text as T+import qualified Data.Text.Encoding.Error as TE+import qualified Data.Text.Lazy as TL+import qualified Data.Text.Lazy.Encoding as TL+import qualified Network.HTTP.Types.Status as HT+import qualified Network.Socket.ByteString.Lazy as Socket+import qualified Network.Wreq as W+import qualified Options.Applicative as O++main :: IO ()+main = do+  issued <- getCurrentTime+  Options {..} <- O.execParser $ O.info (O.helper <*> options) O.fullDesc+  withSystemTempFile "sensu-run.XXX" $ \path hdl -> do+    executed <- getCurrentTime+    rawStatus <- bracket+      (startProcess cmdspec hdl)+      terminateProcess+      (withTimeout timeout . waitForProcess)+    exited <- getCurrentTime+    rawOutput <- BL.readFile path+    let+      encoded = encode $ CheckResult+        { command = cmdspec+        , output = TL.decodeUtf8With TE.lenientDecode rawOutput+        , status = case rawStatus of+          Nothing -> UNKNOWN+          Just ExitSuccess -> OK+          Just (ExitFailure {}) -> CRITICAL+        , duration = diffUTCTime exited executed+        , ..+        }+    if dryRun+      then BL8.putStrLn encoded+      else case endpoint of+        ClientSocketInput port -> sendToClientSocketInput port encoded+        SensuServer urls -> sendToSensuServer urls encoded+    case rawStatus of+      Just ExitSuccess -> exitSuccess+      Nothing -> do+        hPutStrLn stderr $ showCmdSpec cmdspec ++ " timed out"+        exitFailure+      Just (ExitFailure {}) -> exitFailure++sendToClientSocketInput+  :: PortNumber -- ^ Listening port of Sensu client socket+  -> BL8.ByteString -- ^ Payload+  -> IO ()+sendToClientSocketInput port payload = bracket open close $ \sock -> do+  localhost <- inet_addr "127.0.0.1"+  connect sock $ SockAddrInet port localhost+  Socket.sendAll sock payload+  `catch` \(ioe :: IOException) -> do+    hPutStrLn stderr $+      "Failed to write results to localhost:3030 (" ++ show ioe ++ ")"+    exitFailure+  where+    open = socket AF_INET Stream defaultProtocol++sendToSensuServer+  :: NonEmpty String -- ^ Sensu server base URLs+  -> BL8.ByteString -- ^ Payload+  -> IO ()+sendToSensuServer urls payload =+  foldr go (handleError "no more retry") urls+    `catch` \(e :: HttpException) -> handleError (show e)+  where+    go url retry = do+      resp <- W.postWith params (url </> "results") payload+      let status = resp ^. W.responseStatus+      if+        | HT.statusIsClientError status -> handleError $ show status+        | HT.statusIsServerError status -> retry+        | HT.statusIsSuccessful status -> return ()+        | otherwise ->+          fail $ "sendToSensuServer: unexpected status " ++ show status+    params = W.defaults &+      W.header "Content-Type" .~ ["application/json"]+    handleError reason = do+      hPutStrLn stderr $+        "Failed to POST results to Sensu server (" ++ reason ++ ")"+      exitFailure++startProcess :: CmdSpec -> Handle -> IO ProcessHandle+startProcess cmdspec hdl = do+  (_, _, _, ph) <- createProcess CreateProcess+    { cmdspec+    , cwd = Nothing+    , env = Nothing+    , std_in = Inherit+    , std_out = UseHandle hdl+    , std_err = UseHandle hdl+    , close_fds = False+    , create_group = False+    , delegate_ctlc = False+    , detach_console = False+    , create_new_console = False+    , new_session = False+    , child_group = Nothing+    , child_user = Nothing+    }+  return ph++withTimeout :: Maybe NominalDiffTime -> IO a -> IO (Maybe a)+withTimeout time io = case time of+  Just n -> Timeout.timeout (round $ n * 10^(6 :: Int))  io+  Nothing -> Just <$> io++data Options = Options+  { name :: T.Text+  , cmdspec :: CmdSpec+  , source :: Maybe T.Text+  , ttl :: Maybe NominalDiffTime+  , timeout :: Maybe NominalDiffTime+  , handlers :: [T.Text]+  , endpoint :: Endpoint+  , dryRun :: Bool+  }++data Endpoint+  = ClientSocketInput PortNumber+  -- ^ Local client socket input+  | SensuServer (NonEmpty String)+  -- ^ Sensu server API+  --+  -- Multiple servers can be specified. sensu-run retries sequentially until it+  -- succeeds.++options :: O.Parser Options+options = do+  name <- textOption $ mconcat+    [ O.short 'n'+    , O.long "name"+    , O.metavar "NAME"+    , O.help "The name of the check"+    ]+  source <- O.optional $ textOption $ mconcat+    [ O.long "source"+    , O.metavar "SOURCE"+    , O.help $ unlines+      [ "The check source, used to create a JIT Sensu client for an"+      , "external resource" ]+    ]+  ttl <- durationOption $ mconcat+    [ O.long "ttl"+    , O.metavar "SECONDS"+    , O.help "The time to live in seconds until check results are considered stale"+    ]+  timeout <- durationOption $ mconcat+    [ O.long "timeout"+    , O.metavar "SECONDS"+    , O.help "The check executaion duration timeout in seconds"+    ]+  handlers <- O.some $ textOption $ mconcat+    [ O.long "handler"+    , O.metavar "HANDLER"+    , O.help "Sensu event handler(s) to use for events created by the check"+    ]+  endpoint <- asum+    [ ClientSocketInput <$> portOption+    , SensuServer . NE.fromList <$> O.some serverOption+    ]+  dryRun <- O.switch $ mconcat+    [ O.long "dry-run"+    , O.long "dry"+    ]+  cmdspec <- cmdSpecOption+  return Options {..}+  where+    textOption m = T.pack <$> O.strOption m+    durationOption m =+      fmap (realToFrac @Double) <$> O.optional (O.option O.auto m)+    cmdSpecOption = cmdSpec+      <$> O.optional+        (O.switch $ mconcat+          [ O.short 's'+          , O.long "shell"+          , O.help "Execute the command using the shell"+          ])+      <*> O.some (O.strArgument $ O.metavar "COMMAND")+      where+        cmdSpec (fromMaybe False -> isShell) args+          | isShell = ShellCommand (unwords args)+          | otherwise = RawCommand (head args) (tail args)+    portOption = O.option O.auto $ mconcat+      [ O.long "port"+      , O.metavar "PORT"+      , O.help+        "Send results to the local sensu-client listening on the specified port"+      , O.showDefault+      , O.value 3030+      ]+    serverOption = O.strOption $ mconcat+      [ O.long "server"+      , O.metavar "URL"+      , O.help "Send results to the specified Sensu server"+      ]++data CheckResult = CheckResult+  { name :: T.Text+  , command :: CmdSpec+  , status :: ExitCode+  , source :: Maybe T.Text+  , issued :: UTCTime+  , executed :: UTCTime+  , duration :: NominalDiffTime+  , output :: TL.Text+  }++pattern OK :: ExitCode+pattern OK = ExitSuccess++pattern WARNING :: ExitCode+pattern WARNING = ExitFailure 1++pattern CRITICAL :: ExitCode+pattern CRITICAL = ExitFailure 2++pattern UNKNOWN :: ExitCode+pattern UNKNOWN = ExitFailure 3++instance ToJSON CheckResult where+  toJSON = object . checkResultKeyValue+  toEncoding = pairs . mconcat . checkResultKeyValue++checkResultKeyValue :: KeyValue a => CheckResult -> [a]+checkResultKeyValue CheckResult {..} =+  addOptional "source" source+    [ "name" .= name+    , "command" .= showCmdSpec command+    , "issued" .= (floor (utcTimeToPOSIXSeconds issued) :: Int)+    , "executed" .= (floor (utcTimeToPOSIXSeconds executed) :: Int)+    , "duration" .= (round duration :: Int)+    , "status" .= statusToInt status+    , "output" .= output+    ]+    where+      addOptional key val ps = maybe ps (\val' -> key .= val' : ps) val+      statusToInt ExitSuccess = 0+      statusToInt (ExitFailure n) = n++showCmdSpec :: CmdSpec -> String+showCmdSpec = \case+  ShellCommand cmd -> cmd+  RawCommand cmd args -> unwords $ cmd:args