-
Notifications
You must be signed in to change notification settings - Fork 0
/
sensu-run.hs
411 lines (387 loc) · 12.7 KB
/
sensu-run.hs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
{-# LANGUAGE ApplicativeDo #-}
{-# LANGUAGE CPP #-}
{-# 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 Control.Monad
import Data.Foldable
import Data.Function
import Data.List.NonEmpty (NonEmpty)
import Data.Maybe
import Data.Monoid
import System.Exit
import System.IO
import Text.Printf (hPrintf)
import qualified Data.List.NonEmpty as NE
import qualified Data.Version as V
import qualified System.Timeout as Timeout
import Prelude
import Control.Concurrent.Async
import Control.Lens hiding ((.=), (<.>))
import Data.Time
import Data.Time.Clock.POSIX
import Network.HTTP.Client (HttpException)
import Network.HTTP.Client.TLS
import Network.Socket
import System.Directory (removeFile)
import System.FileLock
import System.FilePath ((</>), (<.>))
import System.IO.Temp
import System.PosixCompat.User (getEffectiveUserName)
import System.Process
import qualified Data.ByteString as B
import qualified Data.ByteString.Lazy as BL
import qualified Data.ByteString.Lazy.Char8 as BL8
import qualified Data.ByteString.Lazy.Internal as BLI
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.Builder 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
import System.Process.Kill (killProcessTree)
import qualified Paths_sensu_run as Paths
import Data.Aeson
#if MIN_VERSION_aeson(1, 2, 2)
hiding (Options)
#endif
import System.Posix.Signals
main :: IO ()
main = do
issued <- getCurrentTime
opts <- O.execParser $ O.info (O.helper <*> options) O.fullDesc
case opts of
ShowVersion -> do
putStrLn $ "sensu-run " ++ V.showVersion Paths.version
exitSuccess
RunOptions {..} -> exclusivelyIf lock name $
withSystemTempFile "sensu-run.XXX" $ \path hdl -> do
executed <- getCurrentTime
rawStatus <- try $ bracket
(startProcess cmdspec)
(\(_, _, ph) -> do
terminateProcess ph
killProcessTree ph
waitForProcess ph)
$ \(out, err, ph) -> do
aout <- async $ redirectOutput out $ if redirect
then [hdl, stdout] else [hdl]
aerr <- async $ redirectOutput err $ if redirect
then [hdl, stderr] else [hdl]
status <- withTimeout timeout $ waitForProcess ph
terminateProcess ph
killProcessTree ph
mapM_ waitCatch [aout, aerr]
return status
hClose hdl
exited <- getCurrentTime
rawOutput <- BL.readFile path
user <- T.pack <$> getEffectiveUserName
let
encoded = encode CheckResult
{ command = cmdspec
, output = TL.toLazyText $ mconcat
[ TL.fromLazyText (TL.decodeUtf8With TE.lenientDecode rawOutput)
, case rawStatus of
Left (ioe :: IOException) -> "\n" <> TL.fromString (show ioe)
Right Nothing -> "\n" <> "sensu-run: timed out"
Right _ -> mempty
]
, status = case rawStatus of
Right (Just ExitSuccess) -> OK
Right (Just ExitFailure {}) -> CRITICAL
_ -> UNKNOWN
, 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
Left ioe -> do
hPutStrLn stderr $ show ioe
exitFailure
Right (Just ExitSuccess) -> exitSuccess
Right Nothing -> do
hPutStrLn stderr $ showCmdSpec cmdspec ++ " timed out"
exitFailure
Right (Just ExitFailure {}) -> exitFailure
exclusivelyIf :: Bool -> T.Text -> IO a -> IO a
exclusivelyIf exclusive name io
| exclusive = do
tmpDir <- getCanonicalTemporaryDirectory
let path = tmpDir </> "sensu-run" <.> T.unpack name <.> "lock"
r <- withTryFileLock path Exclusive (const io)
case r of
Nothing -> do
putStrLn $ path ++ " is aquired by other process"
exitSuccess
Just a -> do
removeFile path
return a
| otherwise = io
sendToClientSocketInput
:: PortNumber -- ^ Listening port of Sensu client socket
-> BL8.ByteString -- ^ Payload
-> IO ()
sendToClientSocketInput port payload = bracket open close $ \sock -> do
info <- getAddrInfo (Just hints) (Just "127.0.0.1") (Just $ show port)
case info of
[] -> ioError $ userError "sendToClientSocketInput: failed to get AddrInfo"
localhost:_ -> do
connect sock $ addrAddress localhost
Socket.sendAll sock payload
`catch` \(ioe :: IOException) -> do
hPutStrLn stderr $
"Failed to write results to localhost:" ++ show port
++ " (" ++ show ioe ++ ")"
exitFailure
where
open = socket AF_INET Stream defaultProtocol
hints = defaultHints
{ addrFamily = AF_INET
, addrSocketType = Stream
}
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"]
& W.manager .~ Left tlsManagerSettings
handleError reason = do
hPutStrLn stderr $
"Failed to POST results to Sensu server (" ++ reason ++ ")"
exitFailure
startProcess :: CmdSpec -> IO (Handle, Handle, ProcessHandle)
startProcess cmdspec = do
(_, Just out, Just err, ph) <- createProcess CreateProcess
{ cmdspec
, cwd = Nothing
, env = Nothing
, std_in = Inherit
, std_out = CreatePipe
, std_err = CreatePipe
, close_fds = False
, create_group = True -- necessary to not kill sensu-run itself
, delegate_ctlc = False
, detach_console = False
, create_new_console = False
, new_session = False
, child_group = Nothing
, child_user = Nothing
#if MIN_VERSION_process(1, 5, 0)
, use_process_jobs = True
#endif
}
getPid ph >>= traverse_ installSignalHandlers
return (out, err, ph)
redirectOutput :: Handle -> [Handle] -> IO ()
redirectOutput source sinks = fix $ \loop -> do
eof <- hIsEOF source
unless eof $ do
chunk <- B.hGetSome source BLI.defaultChunkSize
mapM_ (flip B.hPut chunk) sinks
loop
withTimeout :: Maybe NominalDiffTime -> IO a -> IO (Maybe a)
withTimeout time io = case time of
Just n -> Timeout.timeout (seconds n) io
Nothing -> Just <$> io
where
seconds n = round $ n * 10 ^ (6 :: Int)
data Options
= ShowVersion
| RunOptions
{ name :: T.Text
, cmdspec :: CmdSpec
, source :: Maybe T.Text
, ttl :: Maybe NominalDiffTime
, timeout :: Maybe NominalDiffTime
, handlers :: [T.Text]
, endpoint :: Endpoint
, redirect :: Bool
, lock :: Bool
, dryRun :: Bool
}
data Endpoint
= ClientSocketInput PortNumber
-- ^ Local client socket input
| SensuServer (NonEmpty String)
-- ^ Sensu server API or a client HTTP socket
--
-- Multiple HTTP endpoints can be specified. sensu-run retries sequentially
-- until it succeeds. By default Sensu servers listen on port 4567 and
-- client HTTP sockets listen on 3031.
options :: O.Parser Options
options = asum
[ runOptions
, ShowVersion <$ O.switch (O.long "version" <> O.short 'v')
]
where
runOptions = 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
]
redirect <- O.switch $ mconcat
[ O.long "redirect"
, O.help "Redirect command output to sensu-run's output"
]
(not -> lock) <- O.switch $ mconcat
[ O.long "no-lock"
, O.help "Do not create a lock file to allow multiple instances to run"
]
dryRun <- O.switch $ mconcat
[ O.long "dry-run"
, O.long "dry"
, O.help "Dump the JSON object which is supposed to be sent"
]
cmdspec <- cmdSpecOption
return RunOptions {..}
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
, handlers :: [T.Text]
, user :: T.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" .= (realToFrac duration :: Double)
, "status" .= statusToInt status
, "output" .= output
, "handlers" .= handlers
, "user" .= user
]
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
-- | List of signals to trap
signalsToTrap :: [Signal]
signalsToTrap =
[ sigHUP
, sigINT
, sigQUIT
, sigTERM
]
installSignalHandlers :: Pid -> IO ()
installSignalHandlers pid =
for_ signalsToTrap $ \sig ->
installHandler sig (handler sig) (Just reservedSignals)
where
handler sig = CatchOnce $ do
hPrintf stderr
"sensu-run caught signal %s. Resending the signal to PID %s.\n"
(show sig)
(show pid)
signalProcess sig pid