watchmonkey_cli 1.9.0 → 1.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile +3 -0
- data/README.md +14 -2
- data/VERSION +1 -1
- data/doc/checker_example.rb +10 -0
- data/doc/config_example.rb +56 -0
- data/lib/watchmonkey_cli/application/configuration.rb +37 -2
- data/lib/watchmonkey_cli/application/configuration.tpl +22 -1
- data/lib/watchmonkey_cli/application/core.rb +55 -5
- data/lib/watchmonkey_cli/application/dispatch.rb +6 -2
- data/lib/watchmonkey_cli/application/output_helper.rb +4 -0
- data/lib/watchmonkey_cli/application.rb +29 -3
- data/lib/watchmonkey_cli/checker.rb +30 -6
- data/lib/watchmonkey_cli/checkers/dev_pry.rb +29 -0
- data/lib/watchmonkey_cli/checkers/ftp_availability.rb +1 -1
- data/lib/watchmonkey_cli/checkers/ssl_expiration.rb +1 -1
- data/lib/watchmonkey_cli/checkers/unix_defaults.rb +3 -1
- data/lib/watchmonkey_cli/checkers/www_availability.rb +2 -1
- data/lib/watchmonkey_cli/hooks/platypus.rb +2 -2
- data/lib/watchmonkey_cli/hooks/requeue.rb +12 -1
- data/lib/watchmonkey_cli/hooks/telegram_bot.rb +566 -0
- data/lib/watchmonkey_cli/loopback_connection.rb +7 -0
- data/lib/watchmonkey_cli/ssh_connection.rb +11 -1
- data/lib/watchmonkey_cli/version.rb +1 -1
- data/lib/watchmonkey_cli.rb +2 -0
- metadata +5 -3
@@ -0,0 +1,566 @@
|
|
1
|
+
module WatchmonkeyCli
|
2
|
+
class TelegramBot
|
3
|
+
BotProbeError = Class.new(::RuntimeError)
|
4
|
+
UnknownEgressType = Class.new(::RuntimeError)
|
5
|
+
|
6
|
+
class Event
|
7
|
+
attr_reader :app, :bot
|
8
|
+
|
9
|
+
def initialize app, bot, event
|
10
|
+
@app, @bot, @event = app, bot, event
|
11
|
+
end
|
12
|
+
|
13
|
+
def raw
|
14
|
+
@event
|
15
|
+
end
|
16
|
+
|
17
|
+
def method_missing method, *args, &block
|
18
|
+
@event.__send__(method, *args, &block)
|
19
|
+
end
|
20
|
+
|
21
|
+
def user_data
|
22
|
+
app.telegram_bot_get_udata(raw.from.id)
|
23
|
+
end
|
24
|
+
|
25
|
+
def user_admin?
|
26
|
+
user_data && user_data[1].include?(:admin_flag)
|
27
|
+
end
|
28
|
+
|
29
|
+
def from_descriptive
|
30
|
+
"".tap do |name|
|
31
|
+
name << "[BOT] " if from.is_bot
|
32
|
+
name << "#{from.first_name} " if from.first_name.present?
|
33
|
+
name << "#{from.last_name} " if from.last_name.present?
|
34
|
+
name << "(#{from.username}) " if from.username.present?
|
35
|
+
name << "[##{from.id}]"
|
36
|
+
end.strip
|
37
|
+
end
|
38
|
+
|
39
|
+
def message?
|
40
|
+
raw.is_a?(Telegram::Bot::Types::Message)
|
41
|
+
end
|
42
|
+
|
43
|
+
def message
|
44
|
+
return false unless message?
|
45
|
+
raw.text.to_s
|
46
|
+
end
|
47
|
+
|
48
|
+
def chunks
|
49
|
+
@_chunks ||= message.split(" ")
|
50
|
+
end
|
51
|
+
|
52
|
+
def command?
|
53
|
+
raw.is_a?(Telegram::Bot::Types::Message) && message.start_with?("/")
|
54
|
+
end
|
55
|
+
|
56
|
+
def command
|
57
|
+
chunks.first if command?
|
58
|
+
end
|
59
|
+
|
60
|
+
def args
|
61
|
+
chunks[1..-1]
|
62
|
+
end
|
63
|
+
|
64
|
+
def reply msg, msgopts = {}
|
65
|
+
return unless msg.present?
|
66
|
+
msgopts = msgopts.merge(text: msg, chat_id: raw.chat.id)
|
67
|
+
msgopts = msgopts.merge(reply_to_message_id: raw.message_id) unless msgopts[:quote] == false
|
68
|
+
app.telegram_bot_send(msgopts.except(:quote))
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
class BetterQueue
|
73
|
+
def initialize
|
74
|
+
@stor = []
|
75
|
+
@monitor = Monitor.new
|
76
|
+
end
|
77
|
+
|
78
|
+
def sync &block
|
79
|
+
@monitor.synchronize(&block)
|
80
|
+
end
|
81
|
+
|
82
|
+
[:length, :empty?, :push, :pop, :unshift, :shift, :<<].each do |meth|
|
83
|
+
define_method(meth) do |*args, &block|
|
84
|
+
sync { @stor.send(meth, *args, &block) }
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
class PromiseHandler < BetterQueue
|
90
|
+
def scrub!
|
91
|
+
@stor.dup.each_with_index do |p, i|
|
92
|
+
@stor.delete(p) if [:fulfilled, :rejected].include?(p.state)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def await_all!
|
97
|
+
while p = shift
|
98
|
+
p.wait
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def self.hook!(app, bot_opts = {})
|
104
|
+
app.opts[:telegram_bot] = bot_opts.reverse_merge({
|
105
|
+
debug: false,
|
106
|
+
info: true,
|
107
|
+
error: true,
|
108
|
+
throttle_retention: 30.days,
|
109
|
+
retry_on_egress_failure: false,
|
110
|
+
notify: []
|
111
|
+
})
|
112
|
+
|
113
|
+
app.instance_eval do
|
114
|
+
hook :dispatch_around do |action, &act|
|
115
|
+
throw :skip, true unless action == :index
|
116
|
+
begin
|
117
|
+
require "telegram/bot"
|
118
|
+
rescue LoadError
|
119
|
+
abort "[TelegramBot] cannot load telegram/bot, run\n\t# gem install telegram-bot-ruby"
|
120
|
+
end
|
121
|
+
telegram_bot_dispatch(&act)
|
122
|
+
end
|
123
|
+
|
124
|
+
[:debug, :info, :error].each do |level|
|
125
|
+
hook :"on_#{level}" do |msg, robj|
|
126
|
+
telegram_bot_notify(msg, robj)
|
127
|
+
end if app.opts[:telegram_bot][level]
|
128
|
+
end
|
129
|
+
|
130
|
+
hook :wm_shutdown do
|
131
|
+
debug "[TelegramBot] shutting down telegram bot ingress thread"
|
132
|
+
if @telegram_bot_ingress&.alive?
|
133
|
+
@telegram_bot_ingress[:stop] = true
|
134
|
+
@telegram_bot_ingress.join
|
135
|
+
end
|
136
|
+
|
137
|
+
debug "[TelegramBot] shutting down telegram bot egress thread"
|
138
|
+
if @telegram_bot_egress&.alive?
|
139
|
+
telegram_bot_msg_admins "<b>ALERT: Watchmonkey is shutting down!</b>", parse_mode: "HTML" rescue false
|
140
|
+
@telegram_bot_egress_promises.await_all!
|
141
|
+
@telegram_bot_egress[:stop] = true
|
142
|
+
@telegram_bot_egress.join
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
# --------------------
|
147
|
+
|
148
|
+
def telegram_bot_state
|
149
|
+
"#{wm_cfg_path}/telegram_bot.wmstate"
|
150
|
+
end
|
151
|
+
|
152
|
+
def telegram_bot_dispatch &act
|
153
|
+
parent_thread = Thread.current
|
154
|
+
@telegram_bot_throttle_locks = {}
|
155
|
+
@telegram_bot_egress_promises = PromiseHandler.new
|
156
|
+
@telegram_bot_egress_queue = BetterQueue.new
|
157
|
+
telegram_bot_throttle # eager load
|
158
|
+
telegram_bot_normalize_notify_udata
|
159
|
+
|
160
|
+
# ==================
|
161
|
+
# = ingress thread =
|
162
|
+
# ==================
|
163
|
+
debug "[TelegramBot] Starting telegram bot ingress thread..."
|
164
|
+
@telegram_bot_ingress = Thread.new do
|
165
|
+
Thread.current.abort_on_exception = true
|
166
|
+
begin
|
167
|
+
until Thread.current[:stop]
|
168
|
+
begin
|
169
|
+
telegram_bot.fetch_updates {|ev| telegram_bot_handle_event(ev) }
|
170
|
+
rescue SocketError, Faraday::ConnectionFailed => ex
|
171
|
+
error "[TelegramBot] Poll failure, retrying in 3 seconds (#{ex.class}: #{ex.message})"
|
172
|
+
sleep 3 unless Thread.current[:stop]
|
173
|
+
retry unless Thread.current[:stop]
|
174
|
+
end
|
175
|
+
end
|
176
|
+
rescue StandardError => ex
|
177
|
+
parent_thread.raise(ex)
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
# wait for telegram bot to be ready
|
182
|
+
begin
|
183
|
+
Timeout::timeout(10) do
|
184
|
+
loop do
|
185
|
+
break if @telegram_bot_info
|
186
|
+
sleep 0.5
|
187
|
+
end
|
188
|
+
end
|
189
|
+
rescue Timeout::Error
|
190
|
+
abort "[TelegramBot] Failed to start telegram bot within 10 seconds, aborting...", 2
|
191
|
+
end
|
192
|
+
|
193
|
+
# ==================
|
194
|
+
# = egress thread =
|
195
|
+
# ==================
|
196
|
+
debug "[TelegramBot] Starting telegram bot egress thread..."
|
197
|
+
@telegram_bot_egress = Thread.new do
|
198
|
+
Thread.current.abort_on_exception = true
|
199
|
+
begin
|
200
|
+
until Thread.current[:stop] && @telegram_bot_egress_queue.empty?
|
201
|
+
begin
|
202
|
+
promise, item = @telegram_bot_egress_queue.shift
|
203
|
+
if item
|
204
|
+
mode = item.delete(:__mode)
|
205
|
+
case mode
|
206
|
+
when :send then promise.set telegram_bot.api.send_message(item)
|
207
|
+
when :edit then promise.set telegram_bot.api.edit_message_text(item)
|
208
|
+
else raise(UnknownEgressType, "Unknown egress mode `#{mode}'!")
|
209
|
+
end
|
210
|
+
else
|
211
|
+
@telegram_bot_egress_promises.scrub!
|
212
|
+
sleep 0.5
|
213
|
+
end
|
214
|
+
rescue SocketError, Faraday::ConnectionFailed => ex
|
215
|
+
if opts[:telegram_bot][:retry_on_egress_failure]
|
216
|
+
error "[TelegramBot] Push failure, retrying in 3 seconds (#{ex.class}: #{ex.message})"
|
217
|
+
@telegram_bot_egress_queue.unshift([promise, item.merge(__mode: mode)])
|
218
|
+
sleep 3 unless Thread.current[:stop]
|
219
|
+
else
|
220
|
+
error "[TelegramBot] Push failure, discarding message (#{ex.class}: #{ex.message})"
|
221
|
+
sleep 0.5
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
225
|
+
rescue StandardError => ex
|
226
|
+
parent_thread.raise(ex)
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
telegram_bot_msg_admins "<b>INFO: Watchmonkey is watching#{" ONCE" unless opts[:loop_forever]}!</b>", disable_notification: true, parse_mode: "HTML"
|
231
|
+
act.call
|
232
|
+
rescue BotProbeError => ex
|
233
|
+
abort ex.message
|
234
|
+
ensure
|
235
|
+
@telegram_bot_egress_promises.await_all!
|
236
|
+
File.open(telegram_bot_state, "w+") do |f|
|
237
|
+
f << telegram_bot_throttle.to_json
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
def telegram_bot
|
242
|
+
@telegram_bot ||= begin
|
243
|
+
Telegram::Bot::Client.new(opts[:telegram_bot][:api_key], opts[:telegram_bot].except(:api_key)).tap do |bot|
|
244
|
+
begin
|
245
|
+
me = bot.api.get_me
|
246
|
+
if me["ok"]
|
247
|
+
@telegram_bot_info = me["result"]
|
248
|
+
info "[TelegramBot] Established client [#{@telegram_bot_info["id"]}] #{@telegram_bot_info["first_name"]} (#{@telegram_bot_info["username"]})"
|
249
|
+
else
|
250
|
+
raise BotProbeError, "Failed to get telegram client information, got NOK response (#{me.inspect})"
|
251
|
+
end
|
252
|
+
rescue HTTParty::RedirectionTooDeep, Telegram::Bot::Exceptions::ResponseError => ex
|
253
|
+
raise BotProbeError, "Failed to get telegram client information, API key correct? (#{ex.class})"
|
254
|
+
rescue SocketError, Faraday::ConnectionFailed => ex
|
255
|
+
raise BotProbeError, "Failed to get telegram client information, connection failure? (#{ex.class}: #{ex.message})"
|
256
|
+
end
|
257
|
+
end
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
def telegram_bot_send payload
|
262
|
+
Concurrent::Promise.new.tap do |promise|
|
263
|
+
@telegram_bot_egress_queue.push([promise, payload.merge(__mode: :send)])
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
def telegram_bot_edit payload
|
268
|
+
Concurrent::Promise.new.tap do |promise|
|
269
|
+
@telegram_bot_egress_queue.push([promise, payload.merge(__mode: :edit)])
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
def telegram_bot_tid_exclusive tid, &block
|
274
|
+
sync do
|
275
|
+
@telegram_bot_throttle_locks[tid] ||= Monitor.new
|
276
|
+
end
|
277
|
+
@telegram_bot_throttle_locks[tid].synchronize(&block)
|
278
|
+
end
|
279
|
+
|
280
|
+
def telegram_bot_throttle
|
281
|
+
@telegram_bot_throttle ||= begin
|
282
|
+
Hash.new {|h,k| h[k] = Hash.new }.tap do |h|
|
283
|
+
if File.exist?(telegram_bot_state)
|
284
|
+
json = File.read(telegram_bot_state)
|
285
|
+
if json.present?
|
286
|
+
JSON.parse(json).each do |k, v|
|
287
|
+
case k
|
288
|
+
when "__mute_until"
|
289
|
+
h[k] = Time.parse(v)
|
290
|
+
else
|
291
|
+
v.each do |k, data|
|
292
|
+
data[0] = Time.parse(data[0])
|
293
|
+
data[1] = Time.parse(data[1])
|
294
|
+
end
|
295
|
+
v.delete_if {|k, data| data[0] < opts[:telegram_bot][:throttle_retention].ago }
|
296
|
+
h[k.to_i] = v
|
297
|
+
end
|
298
|
+
end
|
299
|
+
end
|
300
|
+
end
|
301
|
+
end
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
305
|
+
def telegram_bot_normalize_notify_udata
|
306
|
+
opts[:telegram_bot][:notify].each do |a|
|
307
|
+
a[1] = (a[1] ? [*a[1]] : [:error]).flat_map(&:to_sym).uniq
|
308
|
+
a[2] ||= {}
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
312
|
+
def telegram_bot_get_udata lookup_tid
|
313
|
+
opts[:telegram_bot][:notify].detect do |tid, level, topts|
|
314
|
+
tid == lookup_tid
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
def telegram_bot_user_muted? tid, notify = true
|
319
|
+
telegram_bot_tid_exclusive(tid) do
|
320
|
+
return false unless mute_until = telegram_bot_throttle[tid]["__mute_until"]
|
321
|
+
if mute_until > Time.current
|
322
|
+
return true
|
323
|
+
else
|
324
|
+
telegram_bot_throttle[tid].delete("__mute_until")
|
325
|
+
telegram_bot_send(text: "You have been unmuted (expired #{mute_until})", chat_id: tid, disable_notification: true) if notify
|
326
|
+
return false
|
327
|
+
end
|
328
|
+
end
|
329
|
+
end
|
330
|
+
|
331
|
+
def telegram_bot_notify msg, robj
|
332
|
+
to_notify = opts[:telegram_bot][:notify].select do |tid, level, topts|
|
333
|
+
level.include?(:all) || level.include?(robj.type)
|
334
|
+
end
|
335
|
+
|
336
|
+
to_notify.each do |tid, level, topts|
|
337
|
+
telegram_bot_tid_exclusive(tid) do
|
338
|
+
next if telegram_bot_user_muted?(tid)
|
339
|
+
if robj
|
340
|
+
# gate only tags
|
341
|
+
next if topts[:only] && topts[:only].any? && !robj.tags.any?{|t| topts[:only].include?(t) }
|
342
|
+
|
343
|
+
# gate except tags
|
344
|
+
next if topts[:except] && topts[:except].any? && robj.tags.any?{|t| topts[:except].include?(t) }
|
345
|
+
|
346
|
+
# send message
|
347
|
+
throttle = telegram_bot_throttle[tid][robj.uniqid] ||= [Time.current, Time.current, 0, nil]
|
348
|
+
throttle[2] += 1
|
349
|
+
if (Time.current - throttle[0]) <= (topts[:throttle] || 0) && throttle[3]
|
350
|
+
throttle[1] = Time.current
|
351
|
+
_telegram_bot_sendmsg(tid, msg, throttle[2], throttle, throttle[3])
|
352
|
+
else
|
353
|
+
throttle[0] = Time.current
|
354
|
+
throttle[2] = 1
|
355
|
+
throttle[3] = nil
|
356
|
+
_telegram_bot_sendmsg(tid, msg, throttle[2], throttle)
|
357
|
+
end
|
358
|
+
else
|
359
|
+
_telegram_bot_sendmsg(tid, msg, 0, [])
|
360
|
+
end
|
361
|
+
end
|
362
|
+
end
|
363
|
+
end
|
364
|
+
|
365
|
+
def _telegram_bot_sendmsg tid, msg, repeat, result_to, msgid = nil
|
366
|
+
msg = "<pre>#{ERB::Util.h msg}</pre>"
|
367
|
+
msg = "#{msg}\n<b>(message repeated #{repeat} times)</b> #{" <i>last occurance: #{result_to[1]}</i>" if result_to[1]}" if repeat > 1 && msgid
|
368
|
+
(msgid ?
|
369
|
+
telegram_bot_edit(chat_id: tid, message_id: msgid, text: msg, disable_web_page_preview: true, parse_mode: "html")
|
370
|
+
:
|
371
|
+
telegram_bot_send(chat_id: tid, text: msg, disable_web_page_preview: true, parse_mode: "html")
|
372
|
+
).tap do |promise|
|
373
|
+
await = Concurrent::Promise.new
|
374
|
+
promise.on_success do |m|
|
375
|
+
if msgid = m.dig("result", "message_id")
|
376
|
+
result_to[3] = msgid
|
377
|
+
await.set :success
|
378
|
+
else
|
379
|
+
puts m.inspect
|
380
|
+
await.set :failed
|
381
|
+
end
|
382
|
+
end
|
383
|
+
promise.on_error do |ex|
|
384
|
+
error "[TelegramBot] MessagePromiseError - #{ex.class}:#{ex.message}"
|
385
|
+
await.set :failed
|
386
|
+
end
|
387
|
+
@telegram_bot_egress_promises << await
|
388
|
+
end
|
389
|
+
rescue
|
390
|
+
end
|
391
|
+
|
392
|
+
def _telegram_bot_timestr_parse *chunks
|
393
|
+
time = Time.current
|
394
|
+
chunks.flatten.each do |chunk|
|
395
|
+
if chunk.end_with?("d")
|
396
|
+
time += chunk[0..-2].to_i.days
|
397
|
+
elsif chunk.end_with?("h")
|
398
|
+
time += chunk[0..-2].to_i.hours
|
399
|
+
elsif chunk.end_with?("m")
|
400
|
+
time += chunk[0..-2].to_i.minutes
|
401
|
+
elsif chunk.end_with?("s")
|
402
|
+
time += chunk[0..-2].to_i.seconds
|
403
|
+
else
|
404
|
+
time += chunk.to_i.seconds
|
405
|
+
end
|
406
|
+
end
|
407
|
+
time
|
408
|
+
end
|
409
|
+
|
410
|
+
def telegram_bot_handle_event ev
|
411
|
+
Event.new(self, telegram_bot, ev).tap do |event|
|
412
|
+
begin
|
413
|
+
ctrl = catch :event_control do
|
414
|
+
case event.command
|
415
|
+
when "/ping"
|
416
|
+
event.reply "Pong!"
|
417
|
+
throw :event_control, :done
|
418
|
+
when "/start"
|
419
|
+
if event.user_data
|
420
|
+
event.reply [].tap{|m|
|
421
|
+
m << "<b>Welcome!</b> I will tell you if something is wrong with your infrastructure."
|
422
|
+
m << "Your current tags are: #{event.user_data[1].join(", ")}"
|
423
|
+
m << "<b>You have admin permissions!</b>" if event.user_admin?
|
424
|
+
m << "\nInstead of muting me in Telegram you can silence me for a while with <code>/mute 6h 30m</code>."
|
425
|
+
m << "Use <code>/help</code> for more information."
|
426
|
+
}.join("\n"), parse_mode: "HTML"
|
427
|
+
else
|
428
|
+
event.reply %{
|
429
|
+
Hello there, unfortunately I don't recognize your user id (#{event.from.id}).
|
430
|
+
Please ask your admin to add you to the configuration file.
|
431
|
+
}
|
432
|
+
end
|
433
|
+
throw :event_control, :done
|
434
|
+
when "/mute"
|
435
|
+
if event.user_data
|
436
|
+
telegram_bot_tid_exclusive(event.from.id) do
|
437
|
+
telegram_bot_user_muted?(event.from.id) # clears if expired
|
438
|
+
if event.args.any?
|
439
|
+
mute_until = _telegram_bot_timestr_parse(event.args)
|
440
|
+
telegram_bot_throttle[event.from.id]["__mute_until"] = mute_until
|
441
|
+
event.reply "You are muted until #{mute_until}\nUse /unmute to prematurely cancel the mute.", disable_notification: true
|
442
|
+
else
|
443
|
+
msg = "Usage: <code>/mute <1d 2h 3m 4s></code>"
|
444
|
+
if mute_until = telegram_bot_throttle[event.from.id]["__mute_until"]
|
445
|
+
msg << "\n<b>You are currently muted until #{mute_until}</b> /unmute"
|
446
|
+
end
|
447
|
+
event.reply msg, parse_mode: "HTML", disable_notification: true
|
448
|
+
end
|
449
|
+
end
|
450
|
+
throw :event_control, :done
|
451
|
+
else
|
452
|
+
throw :event_control, :access_denied
|
453
|
+
end
|
454
|
+
when "/unmute"
|
455
|
+
if event.user_data
|
456
|
+
telegram_bot_tid_exclusive(event.from.id) do
|
457
|
+
if mute_until = telegram_bot_throttle[event.from.id]["__mute_until"]
|
458
|
+
event.reply "You have been unmuted (prematurely canceled #{mute_until})", disable_notification: true
|
459
|
+
else
|
460
|
+
event.reply "You are not currently muted.", disable_notification: true
|
461
|
+
end
|
462
|
+
end
|
463
|
+
throw :event_control, :done
|
464
|
+
else
|
465
|
+
throw :event_control, :access_denied
|
466
|
+
end
|
467
|
+
when "/scan", "/rescan"
|
468
|
+
if event.user_data
|
469
|
+
if respond_to?(:requeue_runall)
|
470
|
+
event.reply "Triggering all tasks in ReQueue…", disable_notification: true
|
471
|
+
requeue_runall
|
472
|
+
else
|
473
|
+
event.reply "Watchmonkey is not running with ReQueue!", disable_notification: true
|
474
|
+
end
|
475
|
+
throw :event_control, :done
|
476
|
+
else
|
477
|
+
throw :event_control, :access_denied
|
478
|
+
end
|
479
|
+
when "/clearthrottle", "/clearthrottles"
|
480
|
+
if event.user_data
|
481
|
+
telegram_bot_tid_exclusive(event.from.id) do
|
482
|
+
was_muted = telegram_bot_throttle[event.from.id].delete("__mute_until")
|
483
|
+
telegram_bot_throttle.delete(event.from.id)
|
484
|
+
event.reply "Cleared all your throttles!", disable_notification: true
|
485
|
+
if was_muted
|
486
|
+
telegram_bot_throttle[event.from.id]["__mute_until"] = was_muted
|
487
|
+
end
|
488
|
+
end
|
489
|
+
throw :event_control, :done
|
490
|
+
else
|
491
|
+
throw :event_control, :access_denied
|
492
|
+
end
|
493
|
+
when "/status"
|
494
|
+
if event.user_admin?
|
495
|
+
msg = []
|
496
|
+
msg << "<pre>"
|
497
|
+
msg << "========== STATUS =========="
|
498
|
+
msg << " Queue: #{@queue.length}"
|
499
|
+
msg << " Requeue: #{@requeue.length}" if @requeue
|
500
|
+
msg << " Workers: #{@threads.select{|t| t[:working] }.length}/#{@threads.length} working (#{@threads.select(&:alive?).length} alive)"
|
501
|
+
msg << " Threads: #{filtered_threads.length}"
|
502
|
+
msg << " Processed: #{@processed}"
|
503
|
+
msg << " Promises: #{@telegram_bot_egress_promises.length}"
|
504
|
+
msg << "========== //STATUS =========="
|
505
|
+
msg << "</pre>"
|
506
|
+
event.reply msg.join("\n"), parse_mode: "HTML", disable_notification: true
|
507
|
+
throw :event_control, :done
|
508
|
+
else
|
509
|
+
throw :event_control, :access_denied
|
510
|
+
end
|
511
|
+
when "/help"
|
512
|
+
event.reply [].tap{|m|
|
513
|
+
m << "<b>/start</b> Bot API"
|
514
|
+
if event.user_data
|
515
|
+
m << "<b>/mute <time></b> Don't send messages for this long (e.g. <code>/mute 1d 2h 3m 4s</code>)"
|
516
|
+
m << "<b>/unmute</b> Cancel mute"
|
517
|
+
m << "<b>/scan</b> Trigger all queued scans in ReQueue"
|
518
|
+
m << "<b>/clearthrottle</b> Clears your throttled messages"
|
519
|
+
end
|
520
|
+
if event.user_admin?
|
521
|
+
m << "<b>/status</b> Some status information"
|
522
|
+
m << "<b>/wm_shutdown</b> Shutdown watchmonkey process, may respawn if deamonized"
|
523
|
+
end
|
524
|
+
}.join("\n"), parse_mode: "HTML", disable_notification: true
|
525
|
+
throw :event_control, :done
|
526
|
+
when "/wm_shutdown"
|
527
|
+
if event.user_admin?
|
528
|
+
$wm_runtime_exiting = true
|
529
|
+
telegram_bot_msg_admins "ALERT: `#{event.from_descriptive}` invoked shutdown!"
|
530
|
+
throw :event_control, :done
|
531
|
+
else
|
532
|
+
throw :event_control, :access_denied
|
533
|
+
end
|
534
|
+
end
|
535
|
+
end
|
536
|
+
|
537
|
+
if ctrl == :access_denied
|
538
|
+
event.reply("You don't have sufficient permissions to execute this command!")
|
539
|
+
elsif ctrl == :bad_request
|
540
|
+
event.reply("Bad Request: Your input is invalid!")
|
541
|
+
end
|
542
|
+
rescue StandardError => ex
|
543
|
+
event.reply("Sorry, encountered an error while processing your request!")
|
544
|
+
|
545
|
+
# notify admins
|
546
|
+
msg = "ERROR: Encountered error while processing a request!\n"
|
547
|
+
msg << "Request: #{ERB::Util.h event.message}\n"
|
548
|
+
msg << "Origin: #{ERB::Util.h event.from.inspect}\n"
|
549
|
+
msg << "<pre>#{ERB::Util.h ex.class}: #{ERB::Util.h ex.message}\n#{ERB::Util.h ex.backtrace.join("\n")}</pre>"
|
550
|
+
telegram_bot_msg_admins(msg, parse_mode: "HTML")
|
551
|
+
end
|
552
|
+
end
|
553
|
+
end
|
554
|
+
|
555
|
+
def telegram_bot_msg_admins msg, msgopts = {}
|
556
|
+
return unless msg.present?
|
557
|
+
opts[:telegram_bot][:notify].each do |tid, level, topts|
|
558
|
+
next unless level.include?(:admin_flag)
|
559
|
+
next if telegram_bot_user_muted?(tid)
|
560
|
+
telegram_bot_send(msgopts.merge(text: msg, chat_id: tid))
|
561
|
+
end
|
562
|
+
end
|
563
|
+
end
|
564
|
+
end
|
565
|
+
end
|
566
|
+
end
|
@@ -1,7 +1,10 @@
|
|
1
1
|
module WatchmonkeyCli
|
2
2
|
class LoopbackConnection
|
3
|
+
attr_reader :opts, :established
|
4
|
+
|
3
5
|
def initialize(id, opts = {}, &initializer)
|
4
6
|
@id = id
|
7
|
+
@established = false
|
5
8
|
@opts = {}.merge(opts)
|
6
9
|
# @mutex = Monitor.new
|
7
10
|
initializer.try(:call, @opts)
|
@@ -15,6 +18,10 @@ module WatchmonkeyCli
|
|
15
18
|
"lo:#{@id}"
|
16
19
|
end
|
17
20
|
|
21
|
+
def established?
|
22
|
+
@established
|
23
|
+
end
|
24
|
+
|
18
25
|
def sync &block
|
19
26
|
# @mutex.synchronize(&block)
|
20
27
|
block.try(:call)
|
@@ -4,6 +4,7 @@ module WatchmonkeyCli
|
|
4
4
|
|
5
5
|
def initialize(id, opts = {}, &initializer)
|
6
6
|
@id = id
|
7
|
+
@established = false
|
7
8
|
|
8
9
|
if opts.is_a?(String)
|
9
10
|
u, h = opts.split("@", 2)
|
@@ -30,6 +31,10 @@ module WatchmonkeyCli
|
|
30
31
|
"ssh:#{@id}"
|
31
32
|
end
|
32
33
|
|
34
|
+
def established?
|
35
|
+
@established
|
36
|
+
end
|
37
|
+
|
33
38
|
def sync &block
|
34
39
|
@mutex.synchronize(&block)
|
35
40
|
end
|
@@ -42,7 +47,12 @@ module WatchmonkeyCli
|
|
42
47
|
end
|
43
48
|
|
44
49
|
def connection
|
45
|
-
sync {
|
50
|
+
sync {
|
51
|
+
@ssh ||= begin
|
52
|
+
@established = true
|
53
|
+
Net::SSH.start(nil, nil, @opts)
|
54
|
+
end
|
55
|
+
}
|
46
56
|
end
|
47
57
|
|
48
58
|
def close!
|
data/lib/watchmonkey_cli.rb
CHANGED
@@ -33,6 +33,7 @@ require "watchmonkey_cli/application/dispatch"
|
|
33
33
|
require "watchmonkey_cli/application"
|
34
34
|
|
35
35
|
# require buildin checkers
|
36
|
+
require "watchmonkey_cli/checkers/dev_pry"
|
36
37
|
require "watchmonkey_cli/checkers/ftp_availability"
|
37
38
|
require "watchmonkey_cli/checkers/mysql_replication"
|
38
39
|
require "watchmonkey_cli/checkers/ssl_expiration"
|
@@ -45,4 +46,5 @@ require "watchmonkey_cli/checkers/unix_file_exists"
|
|
45
46
|
require "watchmonkey_cli/checkers/unix_load"
|
46
47
|
require "watchmonkey_cli/checkers/unix_mdadm"
|
47
48
|
require "watchmonkey_cli/checkers/unix_memory"
|
49
|
+
require "watchmonkey_cli/checkers/unix_cpu_governor"
|
48
50
|
require "watchmonkey_cli/checkers/www_availability"
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: watchmonkey_cli
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.12.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sven Pachnit
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-10-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -123,6 +123,7 @@ files:
|
|
123
123
|
- lib/watchmonkey_cli/application/dispatch.rb
|
124
124
|
- lib/watchmonkey_cli/application/output_helper.rb
|
125
125
|
- lib/watchmonkey_cli/checker.rb
|
126
|
+
- lib/watchmonkey_cli/checkers/dev_pry.rb
|
126
127
|
- lib/watchmonkey_cli/checkers/ftp_availability.rb
|
127
128
|
- lib/watchmonkey_cli/checkers/mysql_replication.rb
|
128
129
|
- lib/watchmonkey_cli/checkers/ssl_expiration.rb
|
@@ -140,6 +141,7 @@ files:
|
|
140
141
|
- lib/watchmonkey_cli/helper.rb
|
141
142
|
- lib/watchmonkey_cli/hooks/platypus.rb
|
142
143
|
- lib/watchmonkey_cli/hooks/requeue.rb
|
144
|
+
- lib/watchmonkey_cli/hooks/telegram_bot.rb
|
143
145
|
- lib/watchmonkey_cli/loopback_connection.rb
|
144
146
|
- lib/watchmonkey_cli/ssh_connection.rb
|
145
147
|
- lib/watchmonkey_cli/version.rb
|
@@ -163,7 +165,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
163
165
|
- !ruby/object:Gem::Version
|
164
166
|
version: '0'
|
165
167
|
requirements: []
|
166
|
-
rubygems_version: 3.1.
|
168
|
+
rubygems_version: 3.1.4
|
167
169
|
signing_key:
|
168
170
|
specification_version: 4
|
169
171
|
summary: Watchmonkey CLI - dead simple agentless monitoring via SSH, HTTP, FTP, etc.
|