flapjack 1.6.0 → 2.0.0b1

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.
Files changed (301) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +4 -6
  3. data/.gitmodules +1 -1
  4. data/.rspec +1 -1
  5. data/.ruby-version +1 -1
  6. data/.travis.yml +12 -13
  7. data/CHANGELOG.md +2 -9
  8. data/CONTRIBUTING.md +7 -2
  9. data/Gemfile +4 -13
  10. data/LICENCE +1 -0
  11. data/README.md +8 -2
  12. data/Rakefile +2 -2
  13. data/bin/flapjack +3 -12
  14. data/build.sh +4 -2
  15. data/etc/flapjack_config.toml.example +273 -0
  16. data/features/ack_after_sched_maint.feature +18 -21
  17. data/features/cli.feature +11 -71
  18. data/features/cli_flapjack-feed-events.feature +14 -15
  19. data/features/cli_flapjack-nagios-receiver.feature +12 -41
  20. data/features/cli_flapper.feature +12 -41
  21. data/features/cli_purge.feature +5 -6
  22. data/features/cli_receive-events.feature +6 -7
  23. data/features/cli_simulate-failed-check.feature +5 -6
  24. data/features/events.feature +206 -181
  25. data/features/events_check_names.feature +4 -7
  26. data/features/notification_rules.feature +144 -223
  27. data/features/notifications.feature +65 -57
  28. data/features/rollup.feature +45 -47
  29. data/features/steps/cli_steps.rb +4 -5
  30. data/features/steps/events_steps.rb +163 -373
  31. data/features/steps/notifications_steps.rb +408 -264
  32. data/features/steps/packaging-lintian_steps.rb +0 -4
  33. data/features/steps/time_travel_steps.rb +0 -26
  34. data/features/support/daemons.rb +6 -31
  35. data/features/support/env.rb +65 -74
  36. data/flapjack.gemspec +22 -24
  37. data/lib/flapjack.rb +14 -7
  38. data/lib/flapjack/cli/flapper.rb +74 -173
  39. data/lib/flapjack/cli/maintenance.rb +278 -109
  40. data/lib/flapjack/cli/migrate.rb +950 -0
  41. data/lib/flapjack/cli/purge.rb +19 -22
  42. data/lib/flapjack/cli/receiver.rb +150 -326
  43. data/lib/flapjack/cli/server.rb +8 -235
  44. data/lib/flapjack/cli/simulate.rb +42 -57
  45. data/lib/flapjack/configuration.rb +51 -37
  46. data/lib/flapjack/coordinator.rb +138 -129
  47. data/lib/flapjack/data/acknowledgement.rb +177 -0
  48. data/lib/flapjack/data/alert.rb +97 -158
  49. data/lib/flapjack/data/check.rb +611 -0
  50. data/lib/flapjack/data/condition.rb +70 -0
  51. data/lib/flapjack/data/contact.rb +226 -456
  52. data/lib/flapjack/data/event.rb +96 -184
  53. data/lib/flapjack/data/extensions/associations.rb +59 -0
  54. data/lib/flapjack/data/extensions/short_name.rb +25 -0
  55. data/lib/flapjack/data/medium.rb +428 -0
  56. data/lib/flapjack/data/metrics.rb +194 -0
  57. data/lib/flapjack/data/notification.rb +22 -281
  58. data/lib/flapjack/data/rule.rb +473 -0
  59. data/lib/flapjack/data/scheduled_maintenance.rb +244 -0
  60. data/lib/flapjack/data/state.rb +221 -0
  61. data/lib/flapjack/data/statistic.rb +112 -0
  62. data/lib/flapjack/data/tag.rb +277 -0
  63. data/lib/flapjack/data/test_notification.rb +182 -0
  64. data/lib/flapjack/data/unscheduled_maintenance.rb +159 -0
  65. data/lib/flapjack/data/validators/id_validator.rb +20 -0
  66. data/lib/flapjack/exceptions.rb +6 -0
  67. data/lib/flapjack/filters/acknowledgement.rb +23 -16
  68. data/lib/flapjack/filters/base.rb +0 -5
  69. data/lib/flapjack/filters/delays.rb +53 -43
  70. data/lib/flapjack/filters/ok.rb +23 -14
  71. data/lib/flapjack/filters/scheduled_maintenance.rb +3 -3
  72. data/lib/flapjack/filters/unscheduled_maintenance.rb +12 -3
  73. data/lib/flapjack/gateways/aws_sns.rb +65 -49
  74. data/lib/flapjack/gateways/aws_sns/alert.text.erb +2 -2
  75. data/lib/flapjack/gateways/aws_sns/alert_subject.text.erb +2 -2
  76. data/lib/flapjack/gateways/aws_sns/rollup_subject.text.erb +1 -1
  77. data/lib/flapjack/gateways/email.rb +107 -90
  78. data/lib/flapjack/gateways/email/alert.html.erb +19 -18
  79. data/lib/flapjack/gateways/email/alert.text.erb +20 -14
  80. data/lib/flapjack/gateways/email/alert_subject.text.erb +2 -1
  81. data/lib/flapjack/gateways/email/rollup.html.erb +14 -13
  82. data/lib/flapjack/gateways/email/rollup.text.erb +13 -10
  83. data/lib/flapjack/gateways/jabber.rb +679 -671
  84. data/lib/flapjack/gateways/jabber/alert.text.erb +9 -6
  85. data/lib/flapjack/gateways/jsonapi.rb +164 -350
  86. data/lib/flapjack/gateways/jsonapi/data/join_descriptor.rb +44 -0
  87. data/lib/flapjack/gateways/jsonapi/data/method_descriptor.rb +21 -0
  88. data/lib/flapjack/gateways/jsonapi/helpers/headers.rb +63 -0
  89. data/lib/flapjack/gateways/jsonapi/helpers/miscellaneous.rb +136 -0
  90. data/lib/flapjack/gateways/jsonapi/helpers/resources.rb +227 -0
  91. data/lib/flapjack/gateways/jsonapi/helpers/serialiser.rb +313 -0
  92. data/lib/flapjack/gateways/jsonapi/helpers/swagger_docs.rb +322 -0
  93. data/lib/flapjack/gateways/jsonapi/methods/association_delete.rb +115 -0
  94. data/lib/flapjack/gateways/jsonapi/methods/association_get.rb +288 -0
  95. data/lib/flapjack/gateways/jsonapi/methods/association_patch.rb +178 -0
  96. data/lib/flapjack/gateways/jsonapi/methods/association_post.rb +116 -0
  97. data/lib/flapjack/gateways/jsonapi/methods/metrics.rb +71 -0
  98. data/lib/flapjack/gateways/jsonapi/methods/resource_delete.rb +119 -0
  99. data/lib/flapjack/gateways/jsonapi/methods/resource_get.rb +186 -0
  100. data/lib/flapjack/gateways/jsonapi/methods/resource_patch.rb +239 -0
  101. data/lib/flapjack/gateways/jsonapi/methods/resource_post.rb +197 -0
  102. data/lib/flapjack/gateways/jsonapi/middleware/array_param_fixer.rb +27 -0
  103. data/lib/flapjack/gateways/jsonapi/{rack → middleware}/json_params_parser.rb +7 -6
  104. data/lib/flapjack/gateways/jsonapi/middleware/request_timestamp.rb +18 -0
  105. data/lib/flapjack/gateways/oobetet.rb +222 -170
  106. data/lib/flapjack/gateways/pager_duty.rb +388 -0
  107. data/lib/flapjack/gateways/pager_duty/alert.text.erb +13 -0
  108. data/lib/flapjack/gateways/slack.rb +56 -48
  109. data/lib/flapjack/gateways/slack/alert.text.erb +1 -1
  110. data/lib/flapjack/gateways/slack/rollup.text.erb +1 -1
  111. data/lib/flapjack/gateways/sms_aspsms.rb +155 -0
  112. data/lib/flapjack/gateways/sms_aspsms/alert.text.erb +7 -0
  113. data/lib/flapjack/gateways/sms_aspsms/rollup.text.erb +2 -0
  114. data/lib/flapjack/gateways/sms_messagenet.rb +77 -57
  115. data/lib/flapjack/gateways/sms_messagenet/alert.text.erb +3 -2
  116. data/lib/flapjack/gateways/sms_nexmo.rb +53 -51
  117. data/lib/flapjack/gateways/sms_nexmo/alert.text.erb +2 -2
  118. data/lib/flapjack/gateways/sms_nexmo/rollup.text.erb +1 -1
  119. data/lib/flapjack/gateways/sms_twilio.rb +79 -62
  120. data/lib/flapjack/gateways/sms_twilio/alert.text.erb +3 -2
  121. data/lib/flapjack/gateways/web.rb +437 -345
  122. data/lib/flapjack/gateways/web/middleware/request_timestamp.rb +18 -0
  123. data/lib/flapjack/gateways/web/public/css/bootstrap.css +3793 -4340
  124. data/lib/flapjack/gateways/web/public/css/bootstrap.css.map +1 -0
  125. data/lib/flapjack/gateways/web/public/fonts/glyphicons-halflings-regular.eot +0 -0
  126. data/lib/flapjack/gateways/web/public/fonts/glyphicons-halflings-regular.svg +273 -214
  127. data/lib/flapjack/gateways/web/public/fonts/glyphicons-halflings-regular.ttf +0 -0
  128. data/lib/flapjack/gateways/web/public/fonts/glyphicons-halflings-regular.woff +0 -0
  129. data/lib/flapjack/gateways/web/public/fonts/glyphicons-halflings-regular.woff2 +0 -0
  130. data/lib/flapjack/gateways/web/public/js/bootstrap.js +1637 -1607
  131. data/lib/flapjack/gateways/web/public/js/self_stats.js +1 -2
  132. data/lib/flapjack/gateways/web/views/_pagination.html.erb +19 -0
  133. data/lib/flapjack/gateways/web/views/check.html.erb +159 -121
  134. data/lib/flapjack/gateways/web/views/checks.html.erb +82 -41
  135. data/lib/flapjack/gateways/web/views/contact.html.erb +59 -71
  136. data/lib/flapjack/gateways/web/views/contacts.html.erb +32 -8
  137. data/lib/flapjack/gateways/web/views/index.html.erb +2 -2
  138. data/lib/flapjack/gateways/web/views/{layout.erb → layout.html.erb} +7 -23
  139. data/lib/flapjack/gateways/web/views/self_stats.html.erb +32 -33
  140. data/lib/flapjack/gateways/web/views/tag.html.erb +32 -0
  141. data/lib/flapjack/gateways/web/views/tags.html.erb +51 -0
  142. data/lib/flapjack/logger.rb +34 -3
  143. data/lib/flapjack/notifier.rb +180 -112
  144. data/lib/flapjack/patches.rb +8 -63
  145. data/lib/flapjack/pikelet.rb +185 -143
  146. data/lib/flapjack/processor.rb +323 -191
  147. data/lib/flapjack/record_queue.rb +33 -0
  148. data/lib/flapjack/redis_proxy.rb +66 -0
  149. data/lib/flapjack/utility.rb +21 -15
  150. data/lib/flapjack/version.rb +2 -1
  151. data/libexec/httpbroker.go +218 -14
  152. data/libexec/oneoff.go +13 -10
  153. data/spec/lib/flapjack/configuration_spec.rb +286 -0
  154. data/spec/lib/flapjack/coordinator_spec.rb +103 -157
  155. data/spec/lib/flapjack/data/check_spec.rb +175 -0
  156. data/spec/lib/flapjack/data/contact_spec.rb +26 -349
  157. data/spec/lib/flapjack/data/event_spec.rb +76 -291
  158. data/spec/lib/flapjack/data/medium_spec.rb +19 -0
  159. data/spec/lib/flapjack/data/rule_spec.rb +43 -0
  160. data/spec/lib/flapjack/data/scheduled_maintenance_spec.rb +976 -0
  161. data/spec/lib/flapjack/data/unscheduled_maintenance_spec.rb +34 -0
  162. data/spec/lib/flapjack/gateways/aws_sns_spec.rb +111 -60
  163. data/spec/lib/flapjack/gateways/email_spec.rb +194 -161
  164. data/spec/lib/flapjack/gateways/jabber_spec.rb +961 -162
  165. data/spec/lib/flapjack/gateways/jsonapi/methods/check_links_spec.rb +155 -0
  166. data/spec/lib/flapjack/gateways/jsonapi/methods/checks_spec.rb +426 -0
  167. data/spec/lib/flapjack/gateways/jsonapi/methods/contact_links_spec.rb +217 -0
  168. data/spec/lib/flapjack/gateways/jsonapi/methods/contacts_spec.rb +425 -0
  169. data/spec/lib/flapjack/gateways/jsonapi/methods/events_spec.rb +271 -0
  170. data/spec/lib/flapjack/gateways/jsonapi/methods/media_spec.rb +257 -0
  171. data/spec/lib/flapjack/gateways/jsonapi/methods/medium_links_spec.rb +163 -0
  172. data/spec/lib/flapjack/gateways/jsonapi/methods/metrics_spec.rb +8 -0
  173. data/spec/lib/flapjack/gateways/jsonapi/methods/rule_links_spec.rb +212 -0
  174. data/spec/lib/flapjack/gateways/jsonapi/methods/rules_spec.rb +289 -0
  175. data/spec/lib/flapjack/gateways/jsonapi/methods/scheduled_maintenance_links_spec.rb +49 -0
  176. data/spec/lib/flapjack/gateways/jsonapi/methods/scheduled_maintenances_spec.rb +242 -0
  177. data/spec/lib/flapjack/gateways/jsonapi/methods/tag_links_spec.rb +274 -0
  178. data/spec/lib/flapjack/gateways/jsonapi/methods/tags_spec.rb +302 -0
  179. data/spec/lib/flapjack/gateways/jsonapi/methods/unscheduled_maintenance_links_spec.rb +49 -0
  180. data/spec/lib/flapjack/gateways/jsonapi/methods/unscheduled_maintenances_spec.rb +339 -0
  181. data/spec/lib/flapjack/gateways/jsonapi_spec.rb +1 -1
  182. data/spec/lib/flapjack/gateways/oobetet_spec.rb +151 -79
  183. data/spec/lib/flapjack/gateways/pager_duty_spec.rb +353 -0
  184. data/spec/lib/flapjack/gateways/slack_spec.rb +53 -53
  185. data/spec/lib/flapjack/gateways/sms_aspsms_spec.rb +106 -0
  186. data/spec/lib/flapjack/gateways/sms_messagenet_spec.rb +111 -54
  187. data/spec/lib/flapjack/gateways/sms_nexmo_spec.rb +50 -51
  188. data/spec/lib/flapjack/gateways/sms_twilio_spec.rb +108 -48
  189. data/spec/lib/flapjack/gateways/web_spec.rb +144 -216
  190. data/spec/lib/flapjack/notifier_spec.rb +132 -1
  191. data/spec/lib/flapjack/pikelet_spec.rb +111 -50
  192. data/spec/lib/flapjack/processor_spec.rb +210 -40
  193. data/spec/lib/flapjack/redis_proxy_spec.rb +45 -0
  194. data/spec/lib/flapjack/utility_spec.rb +11 -15
  195. data/spec/service_consumers/fixture_data.rb +547 -0
  196. data/spec/service_consumers/pact_helper.rb +21 -32
  197. data/spec/service_consumers/pacts/flapjack-diner_v2.0.json +4652 -0
  198. data/spec/service_consumers/provider_states_for_flapjack-diner.rb +279 -322
  199. data/spec/service_consumers/provider_support.rb +8 -0
  200. data/spec/spec_helper.rb +34 -44
  201. data/spec/support/erb_view_helper.rb +1 -1
  202. data/spec/support/factories.rb +58 -0
  203. data/spec/support/jsonapi_helper.rb +15 -26
  204. data/spec/support/mock_logger.rb +43 -0
  205. data/spec/support/xmpp_comparable.rb +24 -0
  206. data/src/flapjack/transport_test.go +30 -1
  207. data/tasks/dump_keys.rake +82 -0
  208. data/tasks/events.rake +7 -7
  209. data/tasks/support/flapjack_config_benchmark.toml +28 -0
  210. data/tasks/support/flapjack_config_benchmark.yaml +0 -2
  211. metadata +175 -222
  212. data/Guardfile +0 -14
  213. data/etc/flapjack_config.yaml.example +0 -477
  214. data/features/cli_flapjack-populator.feature +0 -90
  215. data/features/support/silent_system.rb +0 -4
  216. data/lib/flapjack/cli/import.rb +0 -108
  217. data/lib/flapjack/data/entity.rb +0 -652
  218. data/lib/flapjack/data/entity_check.rb +0 -1044
  219. data/lib/flapjack/data/message.rb +0 -56
  220. data/lib/flapjack/data/migration.rb +0 -234
  221. data/lib/flapjack/data/notification_rule.rb +0 -425
  222. data/lib/flapjack/data/semaphore.rb +0 -44
  223. data/lib/flapjack/data/tagged.rb +0 -48
  224. data/lib/flapjack/gateways/jsonapi/check_methods.rb +0 -206
  225. data/lib/flapjack/gateways/jsonapi/check_presenter.rb +0 -221
  226. data/lib/flapjack/gateways/jsonapi/contact_methods.rb +0 -186
  227. data/lib/flapjack/gateways/jsonapi/entity_methods.rb +0 -223
  228. data/lib/flapjack/gateways/jsonapi/medium_methods.rb +0 -185
  229. data/lib/flapjack/gateways/jsonapi/metrics_methods.rb +0 -132
  230. data/lib/flapjack/gateways/jsonapi/notification_rule_methods.rb +0 -141
  231. data/lib/flapjack/gateways/jsonapi/pagerduty_credential_methods.rb +0 -139
  232. data/lib/flapjack/gateways/jsonapi/report_methods.rb +0 -146
  233. data/lib/flapjack/gateways/pagerduty.rb +0 -318
  234. data/lib/flapjack/gateways/pagerduty/alert.text.erb +0 -10
  235. data/lib/flapjack/gateways/web/public/css/select2-bootstrap.css +0 -87
  236. data/lib/flapjack/gateways/web/public/css/select2.css +0 -615
  237. data/lib/flapjack/gateways/web/public/css/tablesort.css +0 -67
  238. data/lib/flapjack/gateways/web/public/img/select2-spinner.gif +0 -0
  239. data/lib/flapjack/gateways/web/public/img/select2.png +0 -0
  240. data/lib/flapjack/gateways/web/public/img/select2x2.png +0 -0
  241. data/lib/flapjack/gateways/web/public/js/backbone.js +0 -1581
  242. data/lib/flapjack/gateways/web/public/js/backbone.jsonapi.js +0 -322
  243. data/lib/flapjack/gateways/web/public/js/flapjack.js +0 -82
  244. data/lib/flapjack/gateways/web/public/js/jquery.tablesorter.js +0 -1640
  245. data/lib/flapjack/gateways/web/public/js/jquery.tablesorter.widgets.js +0 -1390
  246. data/lib/flapjack/gateways/web/public/js/modules/contact.js +0 -520
  247. data/lib/flapjack/gateways/web/public/js/modules/entity.js +0 -28
  248. data/lib/flapjack/gateways/web/public/js/modules/medium.js +0 -40
  249. data/lib/flapjack/gateways/web/public/js/select2.js +0 -3397
  250. data/lib/flapjack/gateways/web/public/js/tablesort.js +0 -44
  251. data/lib/flapjack/gateways/web/public/js/underscore.js +0 -1276
  252. data/lib/flapjack/gateways/web/views/edit_contacts.html.erb +0 -173
  253. data/lib/flapjack/gateways/web/views/entities.html.erb +0 -30
  254. data/lib/flapjack/gateways/web/views/entity.html.erb +0 -51
  255. data/lib/flapjack/rack_logger.rb +0 -47
  256. data/lib/flapjack/redis_pool.rb +0 -42
  257. data/spec/lib/flapjack/data/entity_check_spec.rb +0 -1418
  258. data/spec/lib/flapjack/data/entity_spec.rb +0 -872
  259. data/spec/lib/flapjack/data/message_spec.rb +0 -30
  260. data/spec/lib/flapjack/data/migration_spec.rb +0 -104
  261. data/spec/lib/flapjack/data/notification_rule_spec.rb +0 -232
  262. data/spec/lib/flapjack/data/notification_spec.rb +0 -53
  263. data/spec/lib/flapjack/data/semaphore_spec.rb +0 -24
  264. data/spec/lib/flapjack/filters/acknowledgement_spec.rb +0 -6
  265. data/spec/lib/flapjack/filters/delays_spec.rb +0 -6
  266. data/spec/lib/flapjack/filters/ok_spec.rb +0 -6
  267. data/spec/lib/flapjack/filters/scheduled_maintenance_spec.rb +0 -6
  268. data/spec/lib/flapjack/filters/unscheduled_maintenance_spec.rb +0 -6
  269. data/spec/lib/flapjack/gateways/jsonapi/check_methods_spec.rb +0 -315
  270. data/spec/lib/flapjack/gateways/jsonapi/check_presenter_spec.rb +0 -223
  271. data/spec/lib/flapjack/gateways/jsonapi/contact_methods_spec.rb +0 -131
  272. data/spec/lib/flapjack/gateways/jsonapi/entity_methods_spec.rb +0 -389
  273. data/spec/lib/flapjack/gateways/jsonapi/medium_methods_spec.rb +0 -231
  274. data/spec/lib/flapjack/gateways/jsonapi/notification_rule_methods_spec.rb +0 -169
  275. data/spec/lib/flapjack/gateways/jsonapi/pagerduty_credential_methods_spec.rb +0 -114
  276. data/spec/lib/flapjack/gateways/jsonapi/report_methods_spec.rb +0 -590
  277. data/spec/lib/flapjack/gateways/pagerduty_spec.rb +0 -249
  278. data/spec/lib/flapjack/gateways/web/views/check.html.erb_spec.rb +0 -21
  279. data/spec/lib/flapjack/gateways/web/views/contact.html.erb_spec.rb +0 -24
  280. data/spec/lib/flapjack/gateways/web/views/index.html.erb_spec.rb +0 -16
  281. data/spec/lib/flapjack/redis_pool_spec.rb +0 -29
  282. data/spec/service_consumers/pacts/flapjack-diner_v1.0.json +0 -4702
  283. data/tasks/entities.rake +0 -151
  284. data/tasks/profile.rake +0 -282
  285. data/tmp/acknowledge.rb +0 -13
  286. data/tmp/create_config_yaml.rb +0 -16
  287. data/tmp/create_event_ok.rb +0 -30
  288. data/tmp/create_event_unknown.rb +0 -30
  289. data/tmp/create_events_failure.rb +0 -34
  290. data/tmp/create_events_ok.rb +0 -32
  291. data/tmp/create_events_ok_fail_ack_ok.rb +0 -53
  292. data/tmp/create_events_ok_failure.rb +0 -41
  293. data/tmp/create_events_ok_failure_ack.rb +0 -53
  294. data/tmp/dummy_contacts.json +0 -43
  295. data/tmp/dummy_entities.json +0 -37
  296. data/tmp/generate_nagios_test_hosts.rb +0 -16
  297. data/tmp/notification_rules.rb +0 -73
  298. data/tmp/parse_config_yaml.rb +0 -7
  299. data/tmp/redis_find_spurious_unknown_states.rb +0 -52
  300. data/tmp/test_json_post.rb +0 -19
  301. data/tmp/test_notification_rules_api.rb +0 -171
@@ -11,20 +11,16 @@
11
11
  }
12
12
  </style>
13
13
 
14
- <p>Hi <%= @alert.contact_first_name%></p>
14
+ <p>Hi <%= @alert.medium.contact.name %></p>
15
15
 
16
16
  <p>Monitoring has detected the following:</p>
17
17
 
18
+ <% check = @alert.check -%>
18
19
  <table>
19
20
  <tbody>
20
- <tr>
21
- <td><strong>Entity</strong></td>
22
- <td><%= @alert.entity %></td>
23
- </tr>
24
-
25
21
  <tr>
26
22
  <td><strong>Check</strong></td>
27
- <td><%= @alert.check %></td>
23
+ <td><%= check.name %></td>
28
24
  </tr>
29
25
 
30
26
  <tr>
@@ -32,45 +28,50 @@
32
28
  <td><%= @alert.state_title_case %></td>
33
29
  </tr>
34
30
 
35
- <% if @alert.summary %>
31
+ <% summary = @alert.summary -%>
32
+ <% unless summary.nil? || summary.empty? -%>
36
33
  <tr>
37
34
  <td><strong>Summary</strong></td>
38
- <td><%= @alert.summary %></td>
35
+ <td><%= summary %></td>
39
36
  </tr>
40
37
  <% end %>
41
38
 
42
- <% if @alert.details %>
39
+ <% details = @alert.details -%>
40
+ <% unless details.nil? || details.empty? -%>
43
41
  <tr>
44
42
  <td><strong>Details</strong></td>
45
- <td><%= @alert.details %></td>
43
+ <td><%= details %></td>
46
44
  </tr>
47
45
  <% end %>
48
46
 
49
- <% if @alert.time %>
47
+ <% time = @alert.time -%>
48
+ <% unless time.nil? -%>
50
49
  <tr>
51
50
  <td><strong>Time</strong></td>
52
- <td><%= Time.at(@alert.time.to_i).to_s %></td>
51
+ <td><%= Time.at(time.to_i).to_s %></td>
53
52
  </tr>
54
53
  <% end %>
55
54
 
56
- <% if @alert.state_duration && @alert.state_duration > 40 %>
55
+ <% condition_duration = @alert.condition_duration -%>
56
+ <% unless condition_duration.nil? || (condition_duration <= 40.0) -%>
57
57
  <tr>
58
58
  <td><strong>Duration</strong></td>
59
- <td><%= ChronicDuration.output(@alert.state_duration, :keep_zero => true) || '0 secs' %></td>
59
+ <td><%= ChronicDuration.output(condition_duration, :keep_zero => true) || '0 secs' %></td>
60
60
  </tr>
61
61
  <% end %>
62
62
 
63
- <% if @alert.last_state %>
63
+ <% unless @alert.last_state.nil? -%>
64
64
  <tr>
65
65
  <td><strong>Previous State</strong></td>
66
66
  <td><%= @alert.last_state_title_case %></td>
67
67
  </tr>
68
68
  <% end %>
69
69
 
70
- <% if @alert.last_summary %>
70
+ <% last_summary = @alert.last_summary -%>
71
+ <% unless last_summary.nil? || last_summary.empty? -%>
71
72
  <tr>
72
73
  <td><strong>Previous Summary</strong></td>
73
- <td><%= @alert.last_summary %></td>
74
+ <td><%= last_summary %></td>
74
75
  </tr>
75
76
  <% end %>
76
77
 
@@ -1,27 +1,33 @@
1
- Hi <%= @alert.contact_first_name %>
1
+ Hi <%= @alert.medium.contact.name %>
2
2
 
3
3
  Monitoring has detected the following:
4
4
 
5
- Entity: <%= @alert.entity %>
6
- Check: <%= @alert.check %>
5
+ <% check = @alert.check -%>
6
+
7
+ Check: <%= check.name %>
7
8
  State: <%= @alert.state_title_case %>
8
- <% if @alert.summary -%>
9
- Summary: <%= @alert.summary %>
9
+ <% summary = @alert.summary -%>
10
+ <% unless summary.nil? || summary.empty? -%>
11
+ Summary: <%= summary %>
10
12
  <% end -%>
11
- <% if @alert.details -%>
12
- Details: <%= @alert.details %>
13
+ <% details = @alert.details -%>
14
+ <% unless details.nil? || details.empty? -%>
15
+ Details: <%= details %>
13
16
  <% end -%>
14
- <% if @alert.time -%>
15
- Time: <%= Time.at(@alert.time.to_i).to_s %>
17
+ <% time = @alert.time -%>
18
+ <% unless time.nil? -%>
19
+ Time: <%= Time.at(time.to_i).to_s %>
16
20
  <% end -%>
17
- <% if @alert.state_duration && @alert.state_duration > 40 -%>
18
- Duration: <%= ChronicDuration.output(@alert.state_duration, :keep_zero => true) || '0 secs' %>
21
+ <% condition_duration = @alert.condition_duration -%>
22
+ <% unless condition_duration.nil? || (condition_duration <= 40) -%>
23
+ Duration: <%= ChronicDuration.output(condition_duration, :keep_zero => true) || '0 secs' %>
19
24
  <% end -%>
20
- <% if @alert.last_state -%>
25
+ <% unless @alert.last_state.nil? -%>
21
26
  Previous State: <%= @alert.last_state_title_case %>
22
27
  <% end -%>
23
- <% if @alert.last_summary -%>
24
- Previous Summary: <%= @alert.last_summary %>
28
+ <% last_summary = @alert.last_summary -%>
29
+ <% unless last_summary.nil? || last_summary.empty? -%>
30
+ Previous Summary: <%= last_summary %>
25
31
  <% end -%>
26
32
 
27
33
  Cheers,
@@ -1,4 +1,5 @@
1
- <%= @alert.type_sentence_case %>: '<%= @alert.check %>' on <%= @alert.entity -%>
1
+ <% check = @alert.check -%>
2
+ <%= @alert.type_sentence_case %>: '<%= check.name %>'
2
3
  <% unless ['acknowledgement', 'test'].include?(@alert.notification_type) -%>
3
4
  is <%= @alert.state_title_case -%>
4
5
  <% end -%>
@@ -11,27 +11,27 @@
11
11
  }
12
12
  </style>
13
13
 
14
- <p>Hi <%= @alert.contact_first_name %></p>
14
+ <p>Hi <%= @alert.contact_name %></p>
15
15
 
16
- <p>You have <%= @alert.rollup_alerts.length %> alerting check<%= @alert.rollup_alerts.length == 1 ? '' : 's' %> as follows:</p>
16
+ <% rollup_alerts_count = @alert.rollup_alerts.length -%>
17
+ <p>You have <%= rollup_alerts_count %> alerting check<%= (rollup_alerts_count == 1) ? '' : 's' %> as follows:</p>
17
18
 
18
19
  <table>
19
20
  <tbody>
20
21
  <tr>
21
22
  <th>Check</th>
22
- <th>Entity</th>
23
23
  <th>State</th>
24
24
  <th>Duration</th>
25
25
  <th>Summary</th>
26
26
  </tr>
27
- <% @alert.rollup_alerts.sort_by {|entity_check, details| details['duration'] }.each do |rollup_alert| -%>
28
- <% r_entity, r_check = rollup_alert[0].split(':', 2) -%>
29
- <% state = rollup_alert[1]['state'] -%>
30
- <% duration = (ChronicDuration.output(rollup_alert[1]['duration'], :keep_zero => true) || '0 secs') -%>
31
- <% summary = rollup_alert[1]['summary'] -%>
27
+
28
+ <% @alert.rollup_alerts.all.sort_by(&:duration).each do |rollup_alert|
29
+ check = rollup_alert.check
30
+ state = rollup_alert.state
31
+ duration = (ChronicDuration.output(rollup_alert.duration, :keep_zero => true) || '0 secs') -%>
32
+ summary = rollup_alert.summary
32
33
  <tr>
33
- <td><%= r_check %></td>
34
- <td><%= r_entity %></td>
34
+ <td><%= check.name %></td>
35
35
  <td><%= ['ok'].include?(state) ? state.upcase : state.titleize %></td>
36
36
  <td><%= duration %></td>
37
37
  <td><%= summary %></td>
@@ -40,10 +40,11 @@
40
40
  </tbody>
41
41
  </table>
42
42
 
43
- <% if @alert.rollup == 'recovery' %>
44
- <p>As your email summary threshold is <%= @alert.rollup_threshold %>, we're taking your email alerts out of summary mode now. You'll now be emailed individually for each alerting check.</p>
43
+ <% rollup_threshold = @alert.medium.rollup_threshold -%>
44
+ <% if 'recovery'.eql?(@alert.rollup) %>
45
+ <p>As your email summary threshold is <%= rollup_threshold %>, we're taking your email alerts out of summary mode now. You'll now be emailed individually for each alerting check.</p>
45
46
  <% else %>
46
- <p>Your email alerts are being summarised as your email summary threshold is set to <%= @alert.rollup_threshold %>. You'll receive summary emails like this one until your number of alerting checks falls below <%= @alert.rollup_threshold %>.</p>
47
+ <p>Your email alerts are being summarised as your email summary threshold is set to <%= rollup_threshold %>. You'll receive summary emails like this one until your number of alerting checks falls below <%= rollup_threshold %>.</p>
47
48
  <% end %>
48
49
 
49
50
  <p>Cheers,<br/>
@@ -1,18 +1,21 @@
1
- Hi <%= @alert.contact_first_name %>
1
+ Hi <%= @alert.contact_name %>
2
2
 
3
- You have <%= @alert.rollup_alerts.length %> alerting check<%= @alert.rollup_alerts.length == 1 ? '' : 's' %> as follows:
3
+ <% rollup_alerts_count = @alert.rollup_alerts.length %>
4
4
 
5
- <% @alert.rollup_alerts.sort_by {|entity_check, details| details['duration'] }.each do |rollup_alert| -%>
6
- <% r_entity, r_check = rollup_alert[0].split(':', 2) -%>
7
- <% state = rollup_alert[1]['state'] -%>
8
- <% duration = (ChronicDuration.output(rollup_alert[1]['duration'], :keep_zero => true) || '0 secs') -%>
9
- * <%= r_check %> on <%= r_entity %> is <%= ['ok'].include?(state) ? state.upcase : state.titleize %> (<%= duration %>)
5
+ You have <%= rollup_alerts_count %> alerting check<%= rollup_alerts_count == 1 ? '' : 's' %> as follows:
6
+
7
+ <% @alert.rollup_alerts.all.sort_by(&:duration).each do |rollup_alert|
8
+ check = rollup_alert.check
9
+ state = rollup_alert.state
10
+ duration = (ChronicDuration.output(rollup_alert.duration, :keep_zero => true) || '0 secs') -%>
11
+ * <%= check.name %> is <%= ['ok'].include?(state) ? state.upcase : state.titleize %> (<%= duration %>)
10
12
  <% end -%>
11
13
 
12
- <% if @alert.rollup == 'recovery' -%>
13
- As your email summary threshold is <%= @alert.rollup_threshold %>, we're taking your email alerts out of summary mode now. You'll now be emailed individually for each alerting check.
14
+ <% rollup_threshold = @alert.medium.rollup_threshold -%>
15
+ <% if 'recovery'.eql?(@alert.rollup) -%>
16
+ As your email summary threshold is <%= rollup_threshold %>, we're taking your email alerts out of summary mode now. You'll now be emailed individually for each alerting check.
14
17
  <% else -%>
15
- Your email alerts are being summarised as your email summary threshold is set to <%= @alert.rollup_threshold %>. You'll receive summary emails like this one until your number of alerting checks falls below <%= @alert.rollup_threshold %>.
18
+ Your email alerts are being summarised as your email summary threshold is set to <%= rollup_threshold %>. You'll receive summary emails like this one until your number of alerting checks falls below <%= rollup_threshold %>.
16
19
  <% end -%>
17
20
 
18
21
  Cheers,
@@ -1,823 +1,831 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require 'em-hiredis'
4
-
3
+ require 'monitor'
5
4
  require 'socket'
6
5
 
7
- require 'blather/client/client'
8
6
  require 'chronic_duration'
7
+ require 'rexml/document'
8
+ require 'xmpp4r/query'
9
+ require 'xmpp4r/muc'
9
10
 
10
- require 'flapjack/data/entity_check'
11
- require 'flapjack/redis_pool'
11
+ require 'flapjack/redis_proxy'
12
+ require 'flapjack/record_queue'
12
13
  require 'flapjack/utility'
14
+ require 'flapjack/exceptions'
13
15
  require 'flapjack/version'
16
+
14
17
  require 'flapjack/data/alert'
18
+ require 'flapjack/data/check'
19
+ require 'flapjack/data/event'
20
+ require 'flapjack/data/scheduled_maintenance'
21
+ require 'flapjack/data/unscheduled_maintenance'
22
+ require 'flapjack/data/tag'
23
+
15
24
 
16
25
  module Flapjack
17
26
 
18
27
  module Gateways
19
28
 
20
- class Jabber < Blather::Client
21
- include Flapjack::Utility
29
+ module Jabber
22
30
 
23
- log = ::Logger.new(STDOUT)
24
- log.level = ::Logger::INFO
25
- Blather.logger = log
31
+ class Notifier
26
32
 
27
- class TextHandler < Nokogiri::XML::SAX::Document
28
- def initialize
29
- @chunks = []
30
- end
33
+ include Flapjack::Utility
31
34
 
32
- attr_reader :chunks
35
+ attr_accessor :siblings
33
36
 
34
- def cdata_block(string)
35
- characters(string)
36
- end
37
+ def initialize(options = {})
38
+ @lock = options[:lock]
39
+ @config = options[:config]
37
40
 
38
- def characters(string)
39
- @chunks << string.strip if string.strip != ""
41
+ # TODO support for config reloading
42
+ @queue = Flapjack::RecordQueue.new(@config['queue'] || 'jabber_notifications',
43
+ Flapjack::Data::Alert)
40
44
  end
41
- end
42
45
 
43
- def initialize(opts = {})
44
- @config = opts[:config]
45
- @redis_config = opts[:redis_config] || {}
46
- @boot_time = opts[:boot_time]
46
+ def start
47
+ begin
48
+ Zermelo.redis = Flapjack.redis
47
49
 
48
- @logger = opts[:logger]
50
+ loop do
51
+ @lock.synchronize do
52
+ @queue.foreach {|alert| handle_alert(alert) }
53
+ end
49
54
 
50
- @redis = Flapjack::RedisPool.new(:config => @redis_config, :size => 2, :logger => @logger)
55
+ @queue.wait
56
+ end
57
+ ensure
58
+ Flapjack.redis.quit
59
+ end
60
+ end
51
61
 
52
- @logger.debug("Jabber Initializing")
62
+ def stop_type
63
+ :exception
64
+ end
53
65
 
54
- @buffer = []
55
- @hostname = Socket.gethostname
66
+ private
56
67
 
57
- # FIXME: i suspect the following should be in #setup so a config reload updates @identifiers
58
- # I moved it here so the rspec passes :-/
59
- @alias = @config['alias'] || 'flapjack'
60
- @identifiers = ((@config['identifiers'] || []) + [@alias]).uniq
61
- @logger.debug("I will respond to the following identifiers: #{@identifiers.join(', ')}")
68
+ def handle_alert(alert)
69
+ @bot ||= @siblings && @siblings.detect {|sib| sib.respond_to?(:announce) }
62
70
 
63
- super()
64
- end
71
+ if @bot.nil?
72
+ Flapjack.logger.warn("jabber bot not running, won't announce")
73
+ return
74
+ end
65
75
 
66
- def stop
67
- @should_quit = true
68
- redis_uri = @redis_config[:path] ||
69
- "redis://#{@redis_config[:host] || '127.0.0.1'}:#{@redis_config[:port] || '6379'}/#{@redis_config[:db] || '0'}"
70
- shutdown_redis = EM::Hiredis.connect(redis_uri)
71
- shutdown_redis.rpush(@config['queue'], Flapjack.dump_json('notification_type' => 'shutdown'))
72
- end
76
+ check = alert.check
77
+ check_name = check.name
73
78
 
74
- def setup
75
- jid = @config['jabberid'] || 'flapjack'
76
- jid += '/' + @hostname unless jid.include?('/')
77
- @flapjack_jid = Blather::JID.new(jid)
79
+ address = alert.address
80
+ state = alert.state
78
81
 
79
- super(@flapjack_jid, @config['password'], @config['server'], @config['port'].to_i)
82
+ Flapjack.logger.debug("processing jabber notification address: #{address}, " +
83
+ "check: '#{check_name}', state: #{state}, summary: #{alert.summary}")
80
84
 
81
- @logger.debug("Building jabber connection with jabberid: " +
82
- @flapjack_jid.to_s + ", port: " + @config['port'].to_s +
83
- ", server: " + @config['server'].to_s + ", password: " +
84
- @config['password'].to_s)
85
+ # event_count = alert.event_count
85
86
 
86
- register_handler :ready do |stanza|
87
- EventMachine::Synchrony.next_tick do
88
- on_ready(stanza)
87
+ @ack_str = if state.eql?('ok') || ['test', 'acknowledgement'].include?(alert.type)
88
+ nil
89
+ else
90
+ "#{@bot.alias}: ACKID #{alert.event_hash}"
89
91
  end
90
- end
91
92
 
92
- body_matchers = @identifiers.inject([]) do |memo, identifier|
93
- @logger.debug("identifier: #{identifier}, memo: #{memo}")
94
- memo << {:body => /^#{identifier}[:\s]/}
95
- memo
96
- end
97
- @logger.debug("body_matchers: #{body_matchers}")
98
- register_handler :message, :groupchat?, body_matchers do |stanza|
99
- EventMachine::Synchrony.next_tick do
100
- on_groupchat(stanza)
101
- end
102
- end
93
+ message_type = alert.rollup ? 'rollup' : 'alert'
103
94
 
104
- register_handler :message, :chat?, :body do |stanza|
105
- EventMachine::Synchrony.next_tick do
106
- on_chat(stanza)
107
- end
108
- end
95
+ message_template_erb, message_template =
96
+ load_template(@config['templates'], message_type,
97
+ 'text', File.join(File.dirname(__FILE__), 'jabber'))
109
98
 
110
- register_handler :disconnected do |stanza|
111
- ret = true
112
- EventMachine::Synchrony.next_tick do
113
- ret = on_disconnect(stanza)
114
- end
115
- ret
116
- end
117
- end
99
+ @alert = alert
100
+ bnd = binding
118
101
 
119
- # Join the MUC Chat room after connecting.
120
- def on_ready(stanza)
121
- return if @should_quit
122
- @connected_at = Time.now.to_i
123
- @logger.info("Jabber Connected")
124
- if @config['rooms'] && @config['rooms'].length > 0
125
- @config['rooms'].each do |room|
126
- @logger.info("Joining room #{room}")
127
- presence = Blather::Stanza::Presence.new
128
- presence.from = @flapjack_jid
129
- presence.to = Blather::JID.new("#{room}/#{@alias}")
130
- presence << "<x xmlns='http://jabber.org/protocol/muc'><history maxstanzas='0'></x>"
131
- EventMachine::Synchrony.next_tick do
132
- write presence
133
- say(room, "flapjack's jabber gateway started at #{Time.now}, hello! Try typing 'help'.", :groupchat)
134
- end
135
- end
136
- end
137
- return if @buffer.empty?
138
- while stanza = @buffer.shift
139
- @logger.debug("Sending a buffered jabber message to: #{stanza.to}, using: #{stanza.type}, message: #{stanza.body}")
140
- EventMachine::Synchrony.next_tick do
141
- write(stanza)
102
+ message = nil
103
+ begin
104
+ message = message_template_erb.result(bnd).chomp
105
+ rescue
106
+ Flapjack.logger.error "Error while executing the ERB for a jabber message, " +
107
+ "ERB being executed: #{message_template}"
108
+ raise
142
109
  end
110
+
111
+ # FIXME: should also check if presence has been established in any group chat rooms that are
112
+ # configured before starting to process events, otherwise the first few may get lost (send
113
+ # before joining the group chat rooms)
114
+ @bot.announce(address, message)
143
115
  end
116
+
144
117
  end
145
118
 
146
- def get_check_details(entity_check, current_time)
147
- sched = entity_check.current_maintenance(:scheduled => true)
148
- unsched = entity_check.current_maintenance(:unscheduled => true)
149
- out = ''
119
+ class Interpreter
150
120
 
151
- if sched.nil? && unsched.nil?
152
- out += "Not in scheduled or unscheduled maintenance.\n"
153
- else
154
- if sched.nil?
155
- out += "Not in scheduled maintenance.\n"
156
- else
157
- start = Time.at(sched[:start_time])
158
- finish = Time.at(sched[:start_time] + sched[:duration])
159
- remain = time_period_in_words( (finish - current_time).ceil )
160
- # TODO a simpler time format?
161
- out += "In scheduled maintenance: #{start} -> #{finish} (#{remain} remaining)\n"
162
- end
121
+ CHECK_MATCH_FRAGMENT = "(?:checks\s+(?:matching\s+\/(.+?)\/|with\s+tag\s+(.+?))|(.+?))"
163
122
 
164
- if unsched.nil?
165
- out += "Not in unscheduled maintenance.\n"
166
- else
167
- start = Time.at(unsched[:start_time])
168
- finish = Time.at(unsched[:start_time] + unsched[:duration])
169
- remain = time_period_in_words( (finish - current_time).ceil )
170
- # TODO a simpler time format?
171
- out += "In unscheduled maintenance: #{start} -> #{finish} (#{remain} remaining)\n"
172
- end
173
- end
123
+ attr_accessor :siblings
174
124
 
175
- out
176
- end
125
+ include Flapjack::Utility
177
126
 
178
- def interpreter(command_raw, from)
179
- msg = nil
180
- action = nil
181
- entity_check = nil
182
-
183
- th = TextHandler.new
184
- parser = Nokogiri::HTML::SAX::Parser.new(th)
185
- parser.parse(command_raw)
186
- command = th.chunks.join(' ')
187
-
188
- case command
189
- when /^ACKID\s+([0-9A-F]+)(?:\s*(.*?)(?:\s*duration:.*?(\w+.*))?)$/im
190
- ackid = $1
191
- comment = $2
192
- duration_str = $3
193
-
194
- error = nil
195
- dur = nil
196
-
197
- if comment.nil? || (comment.length == 0)
198
- error = "please provide a comment, eg \"#{@config['alias']}: ACKID #{$1} AL looking\""
199
- elsif duration_str
200
- # a fairly liberal match above, we'll let chronic_duration do the heavy lifting
201
- dur = ChronicDuration.parse(duration_str)
202
- end
127
+ def initialize(opts = {})
128
+ @lock = opts[:lock]
129
+ @stop_cond = opts[:stop_condition]
130
+ @config = opts[:config]
203
131
 
204
- four_hours = 4 * 60 * 60
205
- duration = (dur.nil? || (dur <= 0)) ? four_hours : dur
132
+ @boot_time = opts[:boot_time]
206
133
 
207
- event_id = @redis.hget('checks_by_hash', ackid)
134
+ @should_quit = false
208
135
 
209
- if event_id.nil?
210
- error = "not found"
211
- else
212
- entity_check = Flapjack::Data::EntityCheck.for_event_id(event_id, :redis => @redis)
213
- error = "unknown entity" if entity_check.nil?
214
- end
136
+ @messages = []
137
+ end
215
138
 
216
- if error
217
- msg = "ERROR - couldn't ACK #{ackid} - #{error}"
218
- else
219
- entity_name, check = event_id.split(':', 2)
139
+ def start
140
+ Zermelo.redis = Flapjack.redis
220
141
 
221
- if entity_check.in_unscheduled_maintenance?
222
- # ack = entity_check.current_maintenance(:unscheduled => true)
223
- # FIXME details from current?
224
- msg = "Changing ACK for #{check} on #{entity_name} (#{ackid})"
225
- else
226
- msg = "ACKing #{check} on #{entity_name} (#{ackid})"
142
+ @lock.synchronize do
143
+
144
+ @bot = self.siblings ? self.siblings.detect {|sib| sib.respond_to?(:announce)} : nil
145
+
146
+ until @messages.empty? && @should_quit
147
+ while msg = @messages.pop
148
+ Flapjack.logger.info "interpreter received #{msg.inspect}"
149
+ interpret(msg[:room], msg[:nick], msg[:time], msg[:message])
150
+ end
151
+ @stop_cond.wait_while { @messages.empty? && !@should_quit }
227
152
  end
228
- action = Proc.new {
229
- Flapjack::Data::Event.create_acknowledgement(
230
- entity_name, check,
231
- :summary => (comment || ''),
232
- :acknowledgement_id => ackid,
233
- :duration => duration,
234
- :redis => @redis
235
- )
236
- }
237
153
  end
154
+ Flapjack.redis.quit
155
+ end
238
156
 
239
- when /^help$/i
240
- msg = "commands: \n" +
241
- " ACKID <id> <comment> [duration: <time spec>]\n" +
242
- " ack entities /pattern/ <comment> [duration: <time spec>]\n" +
243
- " maint entities /pattern/ <comment> [(start-in|start-at): <time spec>] [duration: <time spec>]\n" +
244
- " status entities /pattern/\n" +
245
- " ack checks /check_pattern/ on /entity_pattern/ <comment> [duration: <time spec>]\n" +
246
- " status checks /check_pattern/ on /entity_pattern/\n" +
247
- " find entities matching /pattern/\n" +
248
- " find checks[ matching /pattern/] on (<entity>|entities matching /pattern/)\n" +
249
- " test notifications for <entity>[:<check>]\n" +
250
- " tell me about <entity>[:<check>]\n" +
251
- " identify\n" +
252
- " help\n"
253
-
254
- when /^identify$/i
255
- t = Process.times
256
- fqdn = `/bin/hostname -f`.chomp
257
- pid = Process.pid
258
- msg = "Flapjack #{Flapjack::VERSION} process #{pid} on #{fqdn}\n" +
259
- "Identifiers: #{@identifiers.join(', ')}\n" +
260
- "Boot time: #{@boot_time}\n" +
261
- "User CPU Time: #{t.utime}\n" +
262
- "System CPU Time: #{t.stime}\n" +
263
- `uname -a`.chomp + "\n"
264
-
265
- when /^test notifications for\s+([a-z0-9\-\.]+)(?::(.+))?$/im
266
- entity_name = $1
267
- check_name = $2 || 'test'
268
-
269
- if entity = Flapjack::Data::Entity.find_by_name(entity_name, :redis => @redis)
270
- msg = "so you want me to test notifications for entity: #{entity_name}, check: #{check_name} eh? ... well OK!"
271
-
272
- summary = "Testing notifications to all contacts interested in entity: #{entity_name}, check: #{check_name}"
273
- Flapjack::Data::Event.test_notifications(entity_name, check_name, :summary => summary, :redis => @redis)
274
- else
275
- msg = "yeah, no I can't see #{entity_name} in my systems"
157
+ def stop_type
158
+ :signal
159
+ end
160
+
161
+ def receive_message(room, nick, time, msg)
162
+ @lock.synchronize do
163
+ @messages += [{:room => room, :nick => nick, :time => time, :message => msg}]
164
+ @stop_cond.signal
276
165
  end
166
+ end
277
167
 
278
- when /^tell me about\s+([a-z0-9\-\.]+)(?::(.+))?$+/im
279
- entity_name = $1
280
- check_name = $2
168
+ def get_check_details(check, at_time)
169
+ start_range = Zermelo::Filters::IndexRange.new(nil, at_time, :by_score => true)
170
+ end_range = Zermelo::Filters::IndexRange.new(at_time, nil, :by_score => true)
281
171
 
282
- if entity = Flapjack::Data::Entity.find_by_name(entity_name, :redis => @redis)
283
- check_str = check_name.nil? ? '' : ", check: #{check_name}"
284
- msg = "so you'd like details on entity: #{entity_name}#{check_str} hmm? ... OK!\n"
172
+ sched = check.scheduled_maintenances.intersect(:start_time => start_range,
173
+ :end_time => end_range).all.max_by(&:end_time)
285
174
 
286
- current_time = Time.now
175
+ unsched = check.unscheduled_maintenances.intersect(:start_time => start_range,
176
+ :end_time => end_range).all.max_by(&:end_time)
287
177
 
288
- check_names = check_name.nil? ? entity.check_list.sort : [check_name]
178
+ out = ''
289
179
 
290
- if check_names.empty?
291
- msg += "I couldn't find any checks for entity: #{entity_name}"
180
+ if sched.nil? && unsched.nil?
181
+ out += "Not in scheduled or unscheduled maintenance.\n"
182
+ else
183
+ if sched.nil?
184
+ out += "Not in scheduled maintenance.\n"
292
185
  else
293
- check_names.each do |check|
294
- entity_check = Flapjack::Data::EntityCheck.for_entity(entity, check, :redis => @redis)
295
- next if entity_check.nil?
296
- msg += "---\n#{entity_name}:#{check}\n" if check_name.nil?
297
- msg += get_check_details(entity_check, current_time)
298
- end
186
+ remain = time_period_in_words( (sched.end_time - at_time).ceil )
187
+ # TODO a simpler time format?
188
+ out += "In scheduled maintenance: #{sched.start_time} -> #{sched.end_time} (#{remain} remaining)\n"
189
+ end
190
+
191
+ if unsched.nil?
192
+ out += "Not in unscheduled maintenance.\n"
193
+ else
194
+ remain = time_period_in_words( (unsched.end_time - at_time).ceil )
195
+ # TODO a simpler time format?
196
+ out += "In unscheduled maintenance: #{unsched.start_time} -> #{unsched.end_time} (#{remain} remaining)\n"
299
197
  end
300
- else
301
- msg = "hmmm, I can't see #{entity_name} in my systems"
302
198
  end
303
199
 
304
- when /^(?:find )?checks(?:\s+matching\s+\/(.+)\/)?\s+on\s+(?:entities matching\s+\/(.+)\/|([a-z0-9\-\.]+))/im
305
- check_pattern = $1 ? $1.strip : nil
306
- entity_pattern = $2 ? $2.strip : nil
307
- entity_name = $3
200
+ out
201
+ end
308
202
 
309
- entity_names = if entity_name
310
- [entity_name]
311
- elsif entity_pattern
312
- Flapjack::Data::Entity.find_all_name_matching(entity_pattern, :redis => @redis)
313
- else
314
- []
315
- end
203
+ def derive_check_ids_for(pattern, tag_name, check_name, options = {})
204
+ lock_klasses = options[:lock_klasses] || []
316
205
 
317
- msg = ""
206
+ deriver = if !pattern.nil? && !pattern.strip.empty?
207
+ proc {
208
+ checks = begin
209
+ Flapjack::Data::Check.intersect(:name => Regexp.new(pattern.strip))
210
+ rescue RegexpError
211
+ nil
212
+ end
318
213
 
319
- # hash with entity => check_list, filtered by pattern if required
320
- entities = entity_names.map {|name|
321
- Flapjack::Data::Entity.find_by_name(name, :redis => @redis)
322
- }.compact.inject({}) {|memo, entity|
323
- memo[entity] = entity.check_list.select {|check_name|
324
- !check_pattern || (check_name =~ /#{check_pattern}/i)
214
+ if checks.nil?
215
+ "Error parsing /#{pattern.strip}/"
216
+ else
217
+ num_checks = checks.count
218
+ if num_checks == 0
219
+ "No checks match /#{pattern.strip}/"
220
+ else
221
+ checks = checks.sort(:name, :limit => options[:limit]) if options[:limit]
222
+ yield(checks.ids, "matching /#{pattern.strip}/", num_checks) if block_given?
223
+ end
224
+ end
325
225
  }
326
- memo
327
- }
226
+ elsif !tag_name.nil? && !tag_name.strip.empty?
227
+
228
+ lock_klasses.unshift(Flapjack::Data::Tag)
328
229
 
329
- report_entities = proc {|ents|
330
- ents.inject('') do |memo, (entity, check_list)|
331
- if check_list.empty?
332
- memo += "Entity: #{entity.name} has no checks\n"
230
+ proc {
231
+ tag = Flapjack::Data::Tag.intersect(:name => tag_name.strip).all.first
232
+
233
+ if tag.nil?
234
+ "No tag '#{tag_name.strip}'"
333
235
  else
334
- memo += "Entity: #{entity.name}\nChecks: #{check_list.join(', ')}\n"
236
+ checks = tag.checks
237
+ num_checks = checks.count
238
+ if num_checks == 0
239
+ "No checks with tag '#{tag_name.strip}'"
240
+ else
241
+ checks = checks.sort(:name, :limit => options[:limit]) if options[:limit]
242
+ yield(checks.ids, "with tag '#{tag_name}'", num_checks) if block_given?
243
+ end
335
244
  end
336
- memo += "----\n"
337
- memo
338
- end
339
- }
245
+ }
246
+ elsif !check_name.nil? && !check_name.strip.empty?
247
+ proc {
248
+ checks = Flapjack::Data::Check.intersect(:name => check_name.strip)
340
249
 
341
- case
342
- when entity_pattern
343
- if entities.empty?
344
- msg = "found no entities matching /#{entity_pattern}/"
345
- else
346
- msg = "found #{entities.size} entities matching /#{entity_pattern}/ ... \n" +
347
- report_entities.call(entities)
348
- end
349
- when entity_name
350
- if entities.empty?
351
- msg = "found no entity for '#{entity_name}'"
352
- else
353
- msg = report_entities.call(entities)
354
- end
250
+ if checks.empty?
251
+ "No check exists with name '#{check_name.strip}'"
252
+ else
253
+ num_checks = checks.count
254
+ checks = checks.sort(:name, :limit => options[:limit]) if options[:limit]
255
+ yield(checks.ids, "with name '#{check_name.strip}'", num_checks) if block_given?
256
+ end
257
+ }
355
258
  end
356
259
 
357
- when /^(?:find )?entities matching\s+\/(.+)\//im
358
- pattern = $1.strip
359
- entity_list = Flapjack::Data::Entity.find_all_name_matching(pattern, :redis => @redis)
360
-
361
- if entity_list
362
- max_showable = 30
363
- number_found = entity_list.length
364
- entity_list = entity_list[0..(max_showable - 1)] if number_found > max_showable
365
-
366
- case
367
- when number_found == 0
368
- msg = "found no entities matching /#{pattern}/"
369
- when number_found == 1
370
- msg = "found 1 entity matching /#{pattern}/ ... \n"
371
- when number_found > max_showable
372
- msg = "showing first #{max_showable} of #{number_found} entities found matching /#{pattern}/\n"
373
- else
374
- msg = "found #{number_found} entities matching /#{pattern}/ ... \n"
375
- end
376
- msg += entity_list.join(', ') unless entity_list.empty?
260
+ return if deriver.nil?
377
261
 
262
+ if lock_klasses.empty?
263
+ Flapjack::Data::Check.lock(&deriver)
378
264
  else
379
- msg = "that doesn't seem to be a valid pattern - /#{pattern}/"
265
+ Flapjack::Data::Check.lock(*lock_klasses, &deriver)
380
266
  end
267
+ end
381
268
 
382
- when /^(?:ack )?entities\s+\/(.+)\/(?:\s*(.*?)(?:\s*duration:.*?(\w+.*))?)$/i
383
- entity_pattern = $1.strip
384
- comment = $2 ? $2.strip : nil
385
- duration_str = $3 ? $3.strip : '1 hour'
386
- duration = ChronicDuration.parse(duration_str)
387
- entity_list = Flapjack::Data::Entity.find_all_name_matching(entity_pattern, :redis => @redis)
269
+ def interpret(room, nick, time, command)
270
+ msg = nil
271
+ action = nil
272
+ check = nil
388
273
 
389
- if comment.nil? || (comment.length == 0)
390
- comment = "#{from}: Set via chatbot"
391
- else
392
- comment = "#{from}: #{comment}"
393
- end
274
+ begin
275
+ case command
276
+ when /^help\s*$/
277
+ msg = "commands: \n" +
278
+ " find (number) checks matching /pattern/\n" +
279
+ " find (number) checks with tag <tag>\n" +
280
+ " state of <check>\n" +
281
+ " state of checks matching /pattern/\n" +
282
+ " state of checks with tag <tag>\n" +
283
+ " tell me about <check>\n" +
284
+ " tell me about (number) checks matching /pattern/\n" +
285
+ " tell me about (number) checks with tag <tag>\n" +
286
+ " ACKID <id>[ duration: <time spec>][ comment: <comment>]\n" +
287
+ " ack <check>[ duration: <time spec>][ comment: <comment>]\n" +
288
+ " ack checks matching /pattern/[ duration: <time spec>][ comment: <comment>]\n" +
289
+ " ack checks with tag <tag>[ duration: <time spec>][ comment: <comment>]\n" +
290
+ " maint <check>[ (start-at|start-in): <time spec>][ duration: <time spec>][ comment: <comment>]\n" +
291
+ " maint checks matching /pattern/[ (start-at|start-in): <time spec>][ duration: <time spec>][ comment: <comment>]\n" +
292
+ " maint checks with tag <tag>[ (start-at|start-in): <time spec>][ duration: <time spec>][ comment: <comment>]\n" +
293
+ " test notifications for <check>\n" +
294
+ " test notifications for checks matching /pattern/\n" +
295
+ " test notifications for checks with tag <tag>\n" +
296
+ " identify\n" +
297
+ " help\n"
298
+
299
+ when /^identify\s*$/
300
+ t = Process.times
301
+ fqdn = `/bin/hostname -f`.chomp
302
+ pid = Process.pid
303
+ msg = "Flapjack #{Flapjack::VERSION} process #{pid} on #{fqdn} \n" +
304
+ "Identifiers: #{@bot.identifiers.join(', ')}\n" +
305
+ "Boot time: #{@boot_time}\n" +
306
+ "User CPU Time: #{t.utime}\n" +
307
+ "System CPU Time: #{t.stime}\n" +
308
+ `uname -a`.chomp + "\n"
309
+
310
+ when /^find\s+(\d+\s+)?checks\s+(?:matching\s+\/(.+)\/|with\s+tag\s+(.+))\s*$/im
311
+ limit_checks = Regexp.last_match(1).nil? ? 30 : Regexp.last_match(1).strip.to_i
312
+ pattern = Regexp.last_match(2)
313
+ tag = Regexp.last_match(3)
314
+
315
+ msg = derive_check_ids_for(pattern, tag, nil, :limit => limit_checks) do |check_ids, descriptor, num_checks|
316
+ resp = "Checks #{descriptor}:\n"
317
+ checks = Flapjack::Data::Check.find_by_ids(*check_ids)
318
+ resp += "Showing first #{limit_checks} results of #{num_checks}:\n" if num_checks > limit_checks
319
+ resp += checks.map {|chk|
320
+ cond = chk.condition
321
+ "#{chk.name} is #{cond.nil? ? '[none]' : cond.upcase}"
322
+ }.join(', ')
323
+ resp
324
+ end
394
325
 
395
- if entity_list
396
- number_found = entity_list.length
397
- case
398
- when number_found == 0
399
- msg = "found no entities matching /#{entity_pattern}/"
400
- when number_found >= 1
401
- failing_list = Flapjack::Data::EntityCheck.find_current_names_failing_by_entity(:redis => @redis)
402
- entities = failing_list.select {|k,v| v.count >= 1 && entity_list.include?(k) }
403
- if entities.length >= 1
404
- entities.each_pair do |entity,check_list|
405
- check_list.each do |check|
406
- Flapjack::Data::Event.create_acknowledgement(
407
- entity, check,
408
- :summary => comment,
409
- :duration => duration,
410
- :redis => @redis
411
- )
412
- end
413
- end
414
- msg = entities.inject("Ack list:\n") {|memo,kv|
415
- kv[1].each {|e| memo << "#{kv[0]}:#{e}\n" }
416
- memo
417
- }
418
- else
419
- msg = "found no matching entities with failing checks"
420
- end
421
- else
422
- msg = "that doesn't seem to be a valid pattern - /#{pattern}/"
423
- end
424
- end
326
+ when /^state\s+of\s+#{CHECK_MATCH_FRAGMENT}\s*$/im
327
+ pattern = Regexp.last_match(1)
328
+ tag = Regexp.last_match(2)
329
+ check_name = Regexp.last_match(3)
330
+
331
+ msg = derive_check_ids_for(pattern, tag, check_name) do |check_ids, descriptor|
332
+ "State of checks #{descriptor}:\n" +
333
+ Flapjack::Data::Check.intersect(:id => check_ids).collect {|chk|
334
+ cond = chk.condition
335
+ "#{chk.name} - #{cond.nil? ? '[none]' : cond.upcase}"
336
+ }.join("\n")
337
+ end
425
338
 
426
- when /^(?:maint )?entities\s+\/(.+)\/(?:\s*(.*?)(?:\s*start-in:.*?(\w+.*?))?(?:\s*start-at:.*?(\w+.*?))?(?:\s*duration:.*?(\w+.*?))?)$/i
427
- entity_pattern = $1.strip
428
- comment = $2 ? $2.strip : nil
429
- start_in = $3 ? $3.strip : nil
430
- start_at = $4 ? $4.strip : nil
431
- duration_str = $5 ? $5.strip : '1 hour'
432
- maint_duration = ChronicDuration.parse(duration_str)
433
- entity_names = Flapjack::Data::Entity.find_all_name_matching(entity_pattern, :redis => @redis)
434
-
435
- if comment.nil? || (comment.length == 0)
436
- comment = "#{from}: Set via chatbot"
437
- else
438
- comment = "#{from}: #{comment}"
439
- end
339
+ when /^tell\s+me\s+about\s(\d+\s+)?+#{CHECK_MATCH_FRAGMENT}\s*$/im
340
+ limit_checks = Regexp.last_match(1).nil? ? 30 : Regexp.last_match(1).strip.to_i
341
+ pattern = Regexp.last_match(2)
342
+ tag = Regexp.last_match(3)
343
+ check_name = Regexp.last_match(4)
344
+
345
+ msg = derive_check_ids_for(pattern, tag, check_name, :limit => limit_checks,
346
+ :lock_klasses => [Flapjack::Data::ScheduledMaintenance,
347
+ Flapjack::Data::UnscheduledMaintenance]) do |check_ids, descriptor, num_checks|
348
+ current_time = Time.now
349
+ resp = "Details of checks #{descriptor}\n"
350
+ resp += "Showing first #{limit_checks} results of #{num_checks}:\n" if num_checks > limit_checks
351
+ resp += Flapjack::Data::Check.intersect(:id => check_ids).collect {|chk|
352
+ get_check_details(chk, current_time)
353
+ }.join("")
354
+ end
440
355
 
441
- maint_start = case
442
- when start_in
443
- Time.now.to_i + ChronicDuration.parse(start_in)
444
- when start_at
445
- Chronic.parse(start_at).to_i
446
- else
447
- Time.now.to_i
448
- end
356
+ when /^ACKID\s+([0-9A-F]+)(?:\s*(.*?)(?:\s*duration:.*?(\w+.*))?)$/im
357
+ ackid = Regexp.last_match(1)
358
+ comment = Regexp.last_match(2)
359
+ duration_str = Regexp.last_match(3)
449
360
 
450
- if entity_names
451
- number_found = entity_names.length
452
- case
453
- when number_found == 0
454
- msg = "found no entities matching /#{entity_pattern}/"
455
- when number_found >= 1
456
- entities = entity_names.map {|name|
457
- Flapjack::Data::Entity.find_by_name(name, :redis => @redis)
458
- }.compact.inject({}) {|memo, entity|
459
- memo[entity] = entity.check_list.sort
460
- memo
461
- }
462
- if entities.length >= 1
463
- entities.each_pair do |entity,check_list|
464
- check_list.each do |check|
465
- entity_check = Flapjack::Data::EntityCheck.for_entity(entity, check, :redis => @redis)
466
- entity_check.create_scheduled_maintenance(
467
- maint_start,
468
- maint_duration,
469
- :summary => comment,
470
- )
471
- entity_check.update_current_scheduled_maintenance
472
- end
361
+ error = nil
362
+ dur = nil
363
+
364
+ if comment.nil? || (comment.length == 0)
365
+ error = "please provide a comment, eg \"#{@bot.alias}: ACKID #{Regexp.last_match(1)} AL looking\""
366
+ elsif duration_str
367
+ # a fairly liberal match above, we'll let chronic_duration do the heavy lifting
368
+ dur = ChronicDuration.parse(duration_str)
369
+ end
370
+
371
+ four_hours = 4 * 60 * 60
372
+ duration = (dur.nil? || (dur <= 0)) ? four_hours : dur
373
+
374
+ check = Flapjack::Data::Check.intersect(:ack_hash => ackid).all.first
375
+
376
+ if check.nil?
377
+ msg = "ERROR - couldn't ACK #{ackid} - not found"
378
+ else
379
+ check_name = check.name
380
+
381
+ details = "#{check_name} (#{ackid})"
382
+ if check.in_unscheduled_maintenance?
383
+ msg = "Changing ACK for #{details}"
384
+ else
385
+ msg = "ACKing #{details}"
473
386
  end
474
- msg = entities.inject("Maint list:\n") {|memo,kv|
475
- kv[1].each {|e| memo << "#{kv[0].name}:#{e}\n" }
476
- memo
477
- }
478
- else
479
- msg = "found no matching entities with active checks"
480
- end
481
- else
482
- msg = "that doesn't seem to be a valid pattern - /#{pattern}/"
483
- end
484
- end
485
387
 
486
- when /^(?:status )?entities\s+\/(.+)\/.*$/im
487
- entity_pattern = $1 ? $1.strip : nil
488
- entity_names = Flapjack::Data::Entity.find_all_name_matching(entity_pattern, :redis => @redis)
489
-
490
- if entity_names
491
- number_found = entity_names.length
492
- case
493
- when number_found == 0
494
- msg = "found no entities matching /#{entity_pattern}/"
495
- when number_found >= 1
496
- entities = entity_names.map {|name|
497
- Flapjack::Data::Entity.find_by_name(name, :redis => @redis)
498
- }.compact.inject({}) {|memo, entity|
499
- memo[entity.name] = entity.check_list.map {|check_name|
500
- ec = Flapjack::Data::EntityCheck.for_entity(entity, check_name, :redis => @redis)
501
- "#{check_name}: #{ec.state}"
388
+ action = Proc.new {
389
+ Flapjack::Data::Event.create_acknowledgements(
390
+ @config['processor_queue'] || 'events',
391
+ [check],
392
+ :summary => (comment || ''),
393
+ :acknowledgement_id => ackid,
394
+ :duration => duration,
395
+ )
502
396
  }
503
- memo
504
- }
505
- msg = entities.inject("Status list:\n") {|memo,kv|
506
- kv[1].each {|e| memo << "#{kv[0]}:#{e}\n"}
507
- memo
508
- }
509
- else
510
- msg = "found no matching entities with failing checks"
511
- end
512
- else
513
- msg = "that doesn't seem to be a valid pattern - /#{pattern}/"
514
- end
397
+ end
515
398
 
516
- when /^(?:ack )?checks\s+\/(.+)\/\s+on\s+\/(.+)\/(?:\s*(.*?)(?:\s*duration:.*?(\w+.*))?)$/i
517
- check_pattern = $1.strip
518
- entity_pattern = $2.strip
519
- comment = $3 ? $3.strip : nil
520
- duration_str = $4 ? $4.strip : '1 hour'
521
- duration = ChronicDuration.parse(duration_str)
522
- entity_list = Flapjack::Data::Entity.find_all_name_matching(entity_pattern, :redis => @redis)
399
+ when /^ack\s+#{CHECK_MATCH_FRAGMENT}(?:\s+(.*?)(?:\s*duration:.*?(\w+.*))?)?\s*$/im
400
+ pattern = Regexp.last_match(1)
401
+ tag = Regexp.last_match(2)
402
+ check_name = Regexp.last_match(3)
403
+ comment = Regexp.last_match(4) ? Regexp.last_match(4).strip : nil
404
+ duration_str = Regexp.last_match(5) ? Regexp.last_match(5).strip : '1 hour'
405
+ duration = ChronicDuration.parse(duration_str)
406
+
407
+ msg = derive_check_ids_for(pattern, tag, check_name,
408
+ :lock_klasses => [Flapjack::Data::UnscheduledMaintenance]) do |check_ids, descriptor|
409
+
410
+ failing_checks = Flapjack::Data::Check.
411
+ intersect(:id => check_ids, :failing => true)
412
+
413
+ if failing_checks.empty?
414
+ "No failing checks #{descriptor}"
415
+ else
416
+ summary = "#{nick}: #{comment.blank? ? 'Set via chatbot' : comment}"
417
+
418
+ action = Proc.new {
419
+ Flapjack::Data::Event.create_acknowledgements(
420
+ @config['processor_queue'] || 'events',
421
+ failing_checks,
422
+ :summary => summary,
423
+ :duration => duration
424
+ )
425
+ }
426
+
427
+ "Ack list:\n" + failing_checks.map(&:name).join("\n")
428
+ end
429
+ end
523
430
 
524
- if comment.nil? || (comment.length == 0)
525
- comment = "#{from}: Set via chatbot"
526
- else
527
- comment = "#{from}: #{comment}"
528
- end
431
+ when /^maint\s+#{CHECK_MATCH_FRAGMENT}\s+(?:start-in:.*?(\w+.*?)|start-at:.*?(\w+.*?))?(?:\s+duration:.*?(\w+.*?))?(?:\s+comment:.*?(\w+.*?))?\s*$/im
432
+ pattern = Regexp.last_match(1)
433
+ tag = Regexp.last_match(2)
434
+ check_name = Regexp.last_match(3)
435
+ start_in = Regexp.last_match(4) ? Regexp.last_match(4).strip : nil
436
+ start_at = Regexp.last_match(5) ? Regexp.last_match(5).strip : nil
437
+ duration_str = Regexp.last_match(6) ? Regexp.last_match(6).strip : '1 hour'
438
+ duration = ChronicDuration.parse(duration_str)
439
+ comment = Regexp.last_match(7) ? Regexp.last_match(7).strip : 'Test maintenance'
440
+
441
+ started = case
442
+ when start_in
443
+ Time.now.to_i + ChronicDuration.parse(start_in)
444
+ when start_at
445
+ Chronic.parse(start_at).to_i
446
+ else
447
+ Time.now.to_i
448
+ end
529
449
 
530
- if entity_list
531
- number_found = entity_list.length
532
- case
533
- when number_found == 0
534
- msg = "found no entities matching /#{entity_pattern}/"
535
- when number_found >= 1
450
+ msg = derive_check_ids_for(pattern, tag, check_name,
451
+ :lock_klasses => [Flapjack::Data::ScheduledMaintenance]) do |check_ids, descriptor|
452
+ checks = Flapjack::Data::Check.find_by_ids(*check_ids)
536
453
 
537
- failing_list = Flapjack::Data::EntityCheck.find_current_names_failing_by_entity(:redis => @redis)
454
+ checks.each do |chk|
455
+ sched_maint = Flapjack::Data::ScheduledMaintenance.new(
456
+ :start_time => started,
457
+ :end_time => started + duration,
458
+ :summary => comment
459
+ )
460
+ sched_maint.save
538
461
 
539
- my_failing_checks = Hash[failing_list.map do |k,v|
540
- if entity_list.include?(k)
541
- [k, v.keep_if {|e| e =~ /#{check_pattern}/}].compact
462
+ chk.scheduled_maintenances << sched_maint
542
463
  end
543
- end]
544
- if my_failing_checks.delete_if {|k,v| v.empty? }.length >= 1
545
- my_failing_checks.each_pair do |entity,check_list|
546
- check_list.each do |check|
547
- Flapjack::Data::Event.create_acknowledgement(
548
- entity, check,
549
- :summary => comment,
550
- :duration => duration,
551
- :redis => @redis
552
- )
553
- end
554
- end
555
- msg = my_failing_checks.inject("Ack list:\n") {|memo,kv|
556
- kv[1].each {|e| memo << "#{kv[0]}:#{e}\n" }
557
- memo
464
+
465
+ "Scheduled maintenance for #{duration/60} minutes starting at #{Time.at(started)} on:\n" + checks.collect {|c| "#{c.name}" }.join("\n")
466
+ end
467
+
468
+ when /^test\s+notifications\s+for\s+#{CHECK_MATCH_FRAGMENT}\s*$/im
469
+ pattern = Regexp.last_match(1)
470
+ tag = Regexp.last_match(2)
471
+ check_name = Regexp.last_match(3)
472
+
473
+ msg = derive_check_ids_for(pattern, tag, check_name) do |check_ids, descriptor|
474
+ summary = "Testing notifications to all contacts interested in checks #{descriptor}"
475
+
476
+ checks = Flapjack::Data::Check.find_by_ids(*check_ids)
477
+
478
+ action = Proc.new {
479
+ Flapjack::Data::Event.test_notifications(@config['processor_queue'] || 'events',
480
+ checks, :summary => summary)
558
481
  }
559
- else
560
- msg = "found no matching failing checks"
482
+ "Testing notifications for check#{(checks.size > 1) ? 's' : ''} #{descriptor}"
483
+ end
484
+
485
+ when /^(.*)/
486
+ words = Regexp.last_match(1)
487
+ msg = "what do you mean, '#{words}'? Type 'help' for a list of acceptable commands."
488
+
561
489
  end
562
- else
563
- msg = "that doesn't seem to be a valid pattern - /#{pattern}/"
490
+
491
+ rescue => e
492
+ Flapjack.logger.error { "Exception when interpreting command '#{command}' - #{e.class}, #{e.message}" }
493
+ Flapjack.logger.debug { e.backtrace.join("\n") }
494
+ msg = "Oops, something went wrong processing that command (#{e.class}, #{e.message})"
564
495
  end
565
- end
566
496
 
567
- when /^(?:status )checks\s+\/(.+?)\/(?:\s+on\s+)?(?:\/(.+)?\/)?/i
568
- check_pattern = $1 ? $1.strip : nil
569
- entity_pattern = $2 ? $2.strip : '.*'
570
- entity_names = Flapjack::Data::Entity.find_all_name_matching(entity_pattern, :redis => @redis)
571
-
572
- if entity_names
573
- number_found = entity_names.length
574
- case
575
- when number_found == 0
576
- msg = "found no entities matching /#{entity_pattern}/"
577
- when number_found >= 1
578
- entities = entity_names.map {|name|
579
- Flapjack::Data::Entity.find_by_name(name, :redis => @redis)
580
- }.compact.inject({}) {|memo, entity|
581
- memo[entity.name] = entity.check_list.map {|check_name|
582
- if check_name =~ /#{check_pattern}/
583
- ec = Flapjack::Data::EntityCheck.for_entity(entity, check_name, :redis => @redis)
584
- "#{check_name}: #{ec.state}"
585
- end
586
- }.compact
587
- memo
588
- }
589
- if entities.delete_if {|k,v| v.empty? }.length >= 1
590
- msg = entities.inject("Status list:\n") {|memo,kv|
591
- kv[1].each {|e| memo << "#{kv[0]}:#{e}\n" ; memo }
592
- memo
593
- }
594
- else
595
- msg = "found no matching checks"
596
- end
497
+ @bot ||= @siblings && @siblings.detect {|sib| sib.respond_to?(:announce) }
498
+
499
+ if @bot && (room || nick)
500
+ if room
501
+ Flapjack.logger.info "sending to room #{room}: #{msg}"
502
+ @bot.announce(room, msg)
597
503
  else
598
- msg = "found no matching checks"
504
+ Flapjack.logger.info "sending to user #{nick}: #{msg}"
505
+ @bot.say(nick, msg)
599
506
  end
600
507
  else
601
- msg = "that doesn't seem to be a valid pattern - /#{pattern}/"
508
+ Flapjack.logger.warn "jabber bot not running, won't send #{msg} to #{room || nick}"
602
509
  end
603
510
 
604
- when /^(.*)/
605
- words = $1
606
- msg = "what do you mean, '#{words}'? Type 'help' for a list of acceptable commands."
607
-
511
+ action.call if action
608
512
  end
609
513
 
610
- {:msg => msg, :action => action}
611
514
  end
612
515
 
613
- def on_groupchat(stanza)
614
- return if @should_quit
615
- @logger.debug("groupchat message received: #{stanza.inspect}")
516
+ class Bot
616
517
 
617
- the_command = nil
618
- @identifiers.each do |identifier|
619
- if stanza.body =~ /^#{identifier}:?\s*(.*)/m
620
- the_command = $1
621
- @logger.debug("matched identifier: #{identifier}, command: #{the_command.inspect}")
622
- break
623
- end
624
- end
518
+ attr_accessor :siblings
519
+
520
+ def initialize(opts = {})
521
+ @lock = opts[:lock]
522
+ @stop_cond = opts[:stop_condition]
523
+ @config = opts[:config]
524
+ @boot_time = opts[:boot_time]
625
525
 
626
- from = stanza.from
526
+ @say_buffer = []
527
+ @announce_buffer = []
528
+ @hostname = Socket.gethostname
627
529
 
628
- begin
629
- results = interpreter(the_command, from.to_s)
630
- msg = results[:msg]
631
- action = results[:action]
632
- rescue => e
633
- @logger.debug("Exception when interpreting command '#{the_command}' - #{e.class}, #{e.message}")
634
- msg = "Oops, something went wrong processing that command (#{e.class}, #{e.message})"
530
+ @alias = @config['alias'] || 'flapjack'
531
+ @identifiers = ((@config['identifiers'] || []) + [@alias]).uniq
532
+
533
+ @state_buffer = []
534
+ @should_quit = false
635
535
  end
636
536
 
637
- if msg || action
638
- EventMachine::Synchrony.next_tick do
639
- @logger.info("sending to group chat: #{msg}")
640
- say(from.stripped, msg, :groupchat)
641
- action.call if action
537
+ def alias
538
+ ret = nil
539
+ @lock.synchronize do
540
+ ret = @alias
642
541
  end
542
+ ret
643
543
  end
644
- end
645
-
646
- def on_chat(stanza)
647
- return if @should_quit
648
- @logger.debug("chat message received: #{stanza.inspect}")
649
544
 
650
- if stanza.body =~ /^flapjack:\s+(.*)/m
651
- command = $1
652
- else
653
- command = stanza.body
545
+ def identifiers
546
+ ret = nil
547
+ @lock.synchronize do
548
+ ret = @identifiers
549
+ end
550
+ ret
654
551
  end
655
552
 
656
- from = stanza.from
553
+ def start
554
+ Flapjack.logger.debug("I will respond to the following identifiers: #{@identifiers.join(', ')}")
657
555
 
658
- begin
659
- results = interpreter(command, from.resource.to_s)
660
- msg = results[:msg]
661
- action = results[:action]
662
- rescue => e
663
- @logger.error("Exception when interpreting command '#{command}' - #{e.class}, #{e.message}")
664
- msg = "Oops, something went wrong processing that command (#{e.class}, #{e.message})"
665
- end
556
+ @lock.synchronize do
557
+ interpreter = self.siblings ? self.siblings.detect {|sib| sib.respond_to?(:interpret)} : nil
666
558
 
667
- if msg || action
668
- EventMachine::Synchrony.next_tick do
669
- @logger.info("Sending to #{stanza.from.stripped}: #{msg}")
670
- say(stanza.from.stripped, msg, :chat)
671
- action.call if action
672
- end
673
- end
674
- end
559
+ Flapjack.logger.info("starting")
560
+ Flapjack.logger.debug("new jabber pikelet with the following options: #{@config.inspect}")
675
561
 
676
- def connect_with_retry
677
- attempt = 0
678
- delay = 2
679
- begin
680
- attempt += 1
681
- delay = 10 if attempt > 10
682
- delay = 60 if attempt > 60
683
- EventMachine::Synchrony.sleep(delay || 3) if attempt > 1
684
- @logger.debug("attempting connection to the jabber server")
685
- connect # Blather::Client.connect
686
- rescue StandardError => detail
687
- @logger.error("unable to connect to the jabber server (attempt #{attempt}), retrying in #{delay} seconds ...")
688
- @logger.error("detail: #{detail.message}")
689
- @logger.debug(detail.backtrace.join("\n"))
690
- retry unless @should_quit
691
- end
692
- end
562
+ # ::Jabber::debug = true
693
563
 
694
- # returning true to prevent the reactor loop from stopping
695
- def on_disconnect(stanza)
696
- @logger.warn("disconnect handler called")
697
- return true if @should_quit
698
- @logger.warn("jabbers disconnected! reconnecting after a short deley ...")
699
- EventMachine::Synchrony.sleep(5)
700
- connect_with_retry
701
- true
702
- end
564
+ jabber_id = @config['jabberid'] || 'flapjack'
565
+ jabber_id += '/' + @hostname unless jabber_id.include?('/')
566
+ flapjack_jid = ::Jabber::JID.new(jabber_id)
567
+ client = ::Jabber::Client.new(flapjack_jid)
703
568
 
704
- def say(to, msg, using = :chat, tick = true)
705
- stanza = Blather::Stanza::Message.new(to, msg, using)
706
- if connected?
707
- @logger.debug("Sending a jabber message to: #{to.to_s}, using: #{using.to_s}, message: #{msg}")
708
- write(stanza)
709
- else
710
- @logger.debug("Buffering a jabber message to: #{to.to_s}, using: #{using.to_s}, message: #{msg}")
711
- @buffer << stanza
712
- end
713
- end
569
+ client.on_exception do |exc, stream, loc|
570
+ leave_and_rejoin = nil
571
+
572
+ @lock.synchronize do
573
+
574
+ # called with a nil exception on disconnect for some reason
575
+ if exc
576
+ Flapjack.logger.error exc.class.name
577
+ Flapjack.logger.error ":#{loc.to_s}"
578
+ Flapjack.logger.error exc.message
579
+ Flapjack.logger.error exc.backtrace.join("\n")
580
+ end
714
581
 
715
- def start
716
- @logger.info("starting")
717
- @logger.debug("new jabber pikelet with the following options: #{@config.inspect}")
718
-
719
- # the periodic timer can't be halted early (without doing EM.stop) so
720
- # keep the time short and count the iterations ... could just use
721
- # EM.sleep(1) in a loop, I suppose
722
- ki = 0
723
- keepalive_timer = EventMachine::Synchrony.add_periodic_timer(1) do
724
- ki += 1
725
- if ki == 60
726
- ki = 0
727
- @logger.debug("calling keepalive on the jabber connection")
728
- if connected?
729
- EventMachine::Synchrony.next_tick do
730
- write(' ')
582
+ leave_and_rejoin = @joined && !@should_quit
583
+
584
+ if leave_and_rejoin
585
+ @state_buffer << 'leave'
586
+ @stop_cond.signal
587
+ end
731
588
  end
732
- end
733
- end
734
- end
735
589
 
736
- setup
737
- connect_with_retry
590
+ if leave_and_rejoin
591
+ sleep 3
592
+ @lock.synchronize do
593
+ unless @should_quit
594
+ @state_buffer << 'rejoin'
595
+ @stop_cond.signal
596
+ end
597
+ end
598
+ end
599
+ end
738
600
 
739
- # simplified to use a single queue only as it makes the shutdown logic easier
740
- queue = @config['queue']
741
- events = {}
601
+ check_xml = Proc.new do |data|
602
+ if data.nil?
603
+ nil
604
+ else
605
+ Flapjack.logger.debug "xml_data: #{data}"
606
+ text = ''
607
+ begin
608
+ enc_name = Encoding.default_external.name
609
+ REXML::Document.new("<?xml version=\"1.0\" encoding=\"#{enc_name}\"?>" + data).
610
+ each_element_with_text do |elem|
611
+
612
+ text += elem.texts.join(" ")
613
+ end
614
+ text = data if text.empty? && !data.empty?
615
+ rescue REXML::ParseException
616
+ # invalid XML, so we'll just clear everything inside angled brackets
617
+ text = data.gsub(/<[^>]+>/, '').strip
618
+ end
619
+ text
620
+ end
621
+ end
742
622
 
743
- until @should_quit
623
+ client.add_message_callback do |m|
624
+ unless (m.type != :chat) || m.body.nil? || m.body.strip.empty?
625
+ Flapjack.logger.debug "received message #{m.inspect}"
626
+ nick = m.from
627
+ time = nil
628
+ m.each_element('x') { |x|
629
+ if x.kind_of?(::Jabber::Delay::XDelay)
630
+ time = x.stamp
631
+ end
632
+ }
744
633
 
745
- # FIXME: should also check if presence has been established in any group chat rooms that are
746
- # configured before starting to process events, otherwise the first few may get lost (send
747
- # before joining the group chat rooms)
748
- unless connected?
749
- @logger.debug("not connected, sleep 1 before retry")
750
- EM::Synchrony.sleep(1)
751
- next
752
- end
634
+ unless interpreter.nil?
635
+ interpreter.receive_message(nil, nick, time, check_xml.call(m.body))
636
+ end
637
+ end
638
+ end
753
639
 
754
- @logger.debug("jabber is connected so commencing blpop on #{queue}")
755
- events[queue] = @redis.blpop(queue, 0)
756
- event_json = events[queue][1]
757
- begin
758
- event = Flapjack.load_json(event_json)
640
+ muc_clients = @config['rooms'].inject({}) do |memo, room|
641
+ muc_client = ::Jabber::MUC::SimpleMUCClient.new(client)
642
+ muc_client.on_message do |time, nick, text|
643
+ Flapjack.logger.debug("message #{text} -- #{time} -- #{nick}")
644
+ next if nick == jabber_id
645
+ identifier = identifiers.detect {|id| /^(?:@#{id}|#{id}:)\s*(.*)/m === check_xml.call(text) }
646
+
647
+ unless identifier.nil?
648
+ the_command = Regexp.last_match(1)
649
+ Flapjack.logger.debug("matched identifier: #{identifier}, command: #{the_command.inspect}")
650
+ if interpreter
651
+ interpreter.receive_message(room, nick, time, the_command)
652
+ end
653
+ end
654
+ end
759
655
 
760
- @logger.debug('jabber notification event received: ' + event.inspect)
656
+ memo[room] = muc_client
657
+ memo
658
+ end
761
659
 
762
- if 'shutdown'.eql?(event['notification_type'])
763
- @logger.debug("@should_quit: #{@should_quit}")
764
- if @should_quit
765
- EventMachine::Synchrony.next_tick do
766
- # get delays without the next_tick
767
- close # Blather::Client.close
660
+ attempts_allowed = 3
661
+ attempts_remaining = attempts_allowed
662
+ @joined = false
663
+
664
+ loop do
665
+
666
+ if @joined
667
+ # block this thread until signalled to quit / leave / rejoin
668
+ @stop_cond.wait_until { @should_quit || !@state_buffer.empty? }
669
+ elsif attempts_remaining > 0
670
+ unless @should_quit || (attempts_remaining == attempts_allowed)
671
+ # The only thing that should be interrupting this wait is
672
+ # a pikelet.stop, which would set @should_quit to true;
673
+ # thus we shouldn't see multiple connection attempts happening
674
+ # too quickly.
675
+ @stop_cond.wait(3)
768
676
  end
677
+ unless @should_quit # may have changed during previous wait
678
+ begin
679
+ attempts_remaining -= 1
680
+ _join(client, muc_clients)
681
+ @joined = true
682
+ rescue Errno::ECONNREFUSED, ::Jabber::JabberError => je
683
+ report_error("Couldn't join Jabber server #{@hostname}", je)
684
+ end
685
+ end
686
+ else
687
+ # TODO should we quit Flapjack entirely?
688
+ Flapjack.logger.error "stopping jabber bot, couldn't connect in #{attempts_allowed} attempts"
689
+ @should_quit = true
769
690
  end
770
- next
691
+
692
+ break if @should_quit
693
+ handle_state_change(client, muc_clients) unless @state_buffer.empty?
771
694
  end
772
695
 
773
- alert = Flapjack::Data::Alert.new(event, :logger => @logger)
696
+ # main loop has finished, stop() must have been called -- disconnect
697
+ _leave(client, muc_clients) if client.is_connected?
698
+ end
699
+ end
700
+
701
+ def announce(room, msg)
702
+ @lock.synchronize do
703
+ @announce_buffer += [{:room => room, :msg => msg}]
704
+ @state_buffer << 'announce'
705
+ @stop_cond.signal
706
+ end
707
+ end
774
708
 
775
- @logger.debug("processing jabber notification address: #{alert.address}, entity: #{alert.entity}, " +
776
- "check: '#{alert.check}', state: #{alert.state}, summary: #{alert.summary}")
709
+ def say(nick, msg)
710
+ @lock.synchronize do
711
+ @say_buffer += [{:nick => nick, :msg => msg}]
712
+ @state_buffer << 'say'
713
+ @stop_cond.signal
714
+ end
715
+ end
777
716
 
778
- @ack_str = if alert.state.eql?('ok') || ['test', 'acknowledgement'].include?(alert.type)
779
- nil
717
+ def handle_state_change(client, muc_clients)
718
+ connected = client.is_connected?
719
+ Flapjack.logger.debug "connected? #{connected}"
720
+
721
+ while state = @state_buffer.pop
722
+ Flapjack.logger.debug "state change #{state}"
723
+ case state
724
+ when 'announce'
725
+ _announce(muc_clients) if connected
726
+ when 'say'
727
+ _say(client) if connected
728
+ when 'leave'
729
+ connected ? _leave(client, muc_clients) : _deactivate(muc_clients)
730
+ when 'rejoin'
731
+ _join(client, muc_clients, :rejoin => true) unless connected
780
732
  else
781
- "#{@config['alias']}: ACKID #{alert.event_hash}"
733
+ Flapjack.logger.warn "unknown state change #{state}"
782
734
  end
735
+ end
736
+ end
783
737
 
784
- message_type = alert.rollup ? 'rollup' : 'alert'
738
+ def stop_type
739
+ :signal
740
+ end
785
741
 
786
- message_template_erb, message_template =
787
- load_template(@config['templates'], message_type,
788
- 'text', File.join(File.dirname(__FILE__), 'jabber'))
742
+ def report_error(error_msg, je)
743
+ Flapjack.logger.error error_msg
744
+ message = je.respond_to?(:message) ? je.message : '-'
745
+ Flapjack.logger.error "#{je.class.name} #{message}"
746
+ # if je.respond_to?(:backtrace) && trace = je.backtrace
747
+ # Flapjack.logger.error trace.join("\n")
748
+ # end
749
+ end
789
750
 
790
- @alert = alert
791
- bnd = binding
751
+ def _join(client, muc_clients, opts = {})
752
+ client.connect
753
+ client.auth(@config['password'])
754
+ client.send(::Jabber::Presence.new)
755
+ muc_clients.each_pair do |room, muc_client|
756
+ attempts_allowed = 3
757
+ attempts_remaining = attempts_allowed
758
+ joined = nil
759
+ while !joined && (attempts_remaining > 0)
760
+ @lock.synchronize do
761
+ unless @should_quit || (attempts_remaining == attempts_allowed)
762
+ # The only thing that should be interrupting this wait is
763
+ # a pikelet.stop, which would set @should_quit to true;
764
+ # thus we shouldn't see multiple connection attempts happening
765
+ # too quickly.
766
+ @stop_cond.wait(3)
767
+ end
768
+ end
792
769
 
793
- begin
794
- message = message_template_erb.result(bnd).chomp
795
- rescue => e
796
- @logger.error "Error while executing the ERB for a jabber message, " +
797
- "ERB being executed: #{message_template}"
798
- raise
770
+ # may have changed during previous wait
771
+ sq = nil
772
+ @lock.synchronize do
773
+ sq = @should_quit
774
+ end
775
+
776
+ unless sq
777
+ attempts_remaining -= 1
778
+ begin
779
+ muc_client.join(room + '/' + @alias, nil, :history => false)
780
+ t = Time.now
781
+ msg = opts[:rejoin] ? "flapjack jabber gateway rejoining at #{t}, hello again!" :
782
+ "flapjack jabber gateway started at #{t}, hello! Try typing 'help'."
783
+ muc_client.say(msg) if @config['chatbot_announce']
784
+ joined = true
785
+ rescue Errno::ECONNREFUSED, ::Jabber::JabberError => muc_je
786
+ report_error("Couldn't join MUC room #{room}, #{attempts_remaining} attempts remaining", muc_je)
787
+ raise if attempts_remaining <= 0
788
+ joined = false
789
+ end
790
+ end
799
791
  end
792
+ end
793
+ end
794
+
795
+ def _leave(client, muc_clients)
796
+ if @joined
797
+ muc_clients.values.each {|muc_client| muc_client.exit if muc_client.active? }
798
+ client.close
799
+ end
800
+ @joined = false
801
+ end
802
+
803
+ def _deactivate(muc_clients)
804
+ # send method has been overridden in MUCClient class
805
+ # without this MUC clients will still think they are active
806
+ muc_clients.values.each {|muc_client| muc_client.__send__(:deactivate) }
807
+ end
800
808
 
801
- chat_type = :chat
802
- chat_type = :groupchat if @config['rooms'] && @config['rooms'].include?(alert.address)
803
- EventMachine::Synchrony.next_tick do
804
- say(Blather::JID.new(alert.address), message, chat_type)
805
- alert.record_send_success!
809
+ def _announce(muc_clients)
810
+ @announce_buffer.each do |announce|
811
+ if (muc_client = muc_clients[announce[:room]]) && muc_client.active?
812
+ muc_client.say(announce[:msg])
813
+ announce[:sent] = true
806
814
  end
815
+ end
816
+ @announce_buffer.delete_if {|announce| announce[:sent] }
817
+ end
807
818
 
808
- rescue => e
809
- # TODO: have non-fatal errors generate messages (eg via flapjack events or straight to
810
- # rollbar or similar)
811
- @logger.error "Error generating or dispatching jabber message: #{e.class}: #{e.message}\n" +
812
- e.backtrace.join("\n")
813
- @logger.debug "Message that could not be processed: \n" + event_json
819
+ def _say(client)
820
+ while speak = @say_buffer.pop
821
+ msg = ::Jabber::Message::new(speak[:nick], speak[:msg])
822
+ msg.type = :chat
823
+ client.send( msg )
814
824
  end
815
825
  end
816
826
 
817
- keepalive_timer.cancel
818
827
  end
819
828
 
820
829
  end
821
-
822
830
  end
823
831
  end