flapjack 0.5.5 → 0.6.23

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 (167) hide show
  1. data/.gitignore +10 -0
  2. data/.rbenv-version +1 -0
  3. data/.rspec +10 -0
  4. data/Gemfile +18 -0
  5. data/Guardfile +14 -0
  6. data/README.md +152 -173
  7. data/Rakefile +53 -150
  8. data/bin/flapjack +72 -0
  9. data/bin/flapjack-nagios-receiver +111 -0
  10. data/bin/flapjack-nagios-receiver-control +15 -0
  11. data/bin/flapjack-netsaint-parser +0 -2
  12. data/bin/flapjack-populator +133 -16
  13. data/bin/install-flapjack-systemwide +2 -2
  14. data/config.ru +11 -0
  15. data/dist/etc/init.d/flapjack +46 -0
  16. data/dist/etc/init.d/flapjack-nagios-receiver +36 -0
  17. data/doc/GLOSSARY.md +19 -0
  18. data/etc/flapjack_config.yaml.example +90 -0
  19. data/features/events.feature +132 -0
  20. data/features/notifications.feature +57 -0
  21. data/features/packaging-lintian.feature +5 -3
  22. data/features/steps/events_steps.rb +164 -0
  23. data/features/steps/flapjack-importer_steps.rb +2 -5
  24. data/features/steps/flapjack-worker_steps.rb +13 -6
  25. data/features/steps/notifications_steps.rb +178 -0
  26. data/features/steps/packaging-lintian_steps.rb +14 -0
  27. data/features/steps/time_travel_steps.rb +34 -0
  28. data/features/support/env.rb +63 -36
  29. data/flapjack.gemspec +35 -186
  30. data/lib/flapjack.rb +2 -0
  31. data/lib/flapjack/api.rb +274 -0
  32. data/lib/flapjack/api/entity_check_presenter.rb +184 -0
  33. data/lib/flapjack/api/entity_presenter.rb +66 -0
  34. data/lib/flapjack/cli/worker_manager.rb +1 -2
  35. data/lib/flapjack/configuration.rb +11 -0
  36. data/lib/flapjack/coordinator.rb +288 -0
  37. data/lib/flapjack/daemonizing.rb +186 -0
  38. data/lib/flapjack/data/contact.rb +45 -0
  39. data/lib/flapjack/data/entity.rb +89 -0
  40. data/lib/flapjack/data/entity_check.rb +396 -0
  41. data/lib/flapjack/data/event.rb +144 -0
  42. data/lib/flapjack/data/notification.rb +13 -0
  43. data/lib/flapjack/executive.rb +289 -0
  44. data/lib/flapjack/filters/acknowledgement.rb +39 -0
  45. data/lib/flapjack/filters/{any_parents_failed.rb → base.rb} +6 -4
  46. data/lib/flapjack/filters/delays.rb +53 -0
  47. data/lib/flapjack/filters/detect_mass_client_failures.rb +44 -0
  48. data/lib/flapjack/filters/ok.rb +25 -5
  49. data/lib/flapjack/filters/scheduled_maintenance.rb +17 -0
  50. data/lib/flapjack/filters/unscheduled_maintenance.rb +17 -0
  51. data/lib/flapjack/jabber.rb +294 -0
  52. data/lib/flapjack/notification/common.rb +23 -0
  53. data/lib/flapjack/notification/email.rb +107 -0
  54. data/lib/flapjack/notification/email/alert.html.haml +48 -0
  55. data/lib/flapjack/notification/email/alert.text.erb +14 -0
  56. data/lib/flapjack/notification/sms.rb +42 -0
  57. data/lib/flapjack/notification/sms/messagenet.rb +49 -0
  58. data/lib/flapjack/notifier_engine.rb +4 -4
  59. data/lib/flapjack/notifiers/mailer/mailer.rb +6 -7
  60. data/lib/flapjack/notifiers/xmpp/xmpp.rb +12 -12
  61. data/lib/flapjack/pagerduty.rb +230 -0
  62. data/lib/flapjack/patches.rb +108 -19
  63. data/lib/flapjack/persistence/data_mapper/models/check.rb +5 -3
  64. data/lib/flapjack/persistence/data_mapper/models/check_template.rb +2 -0
  65. data/lib/flapjack/persistence/data_mapper/models/event.rb +2 -0
  66. data/lib/flapjack/persistence/data_mapper/models/node.rb +3 -1
  67. data/lib/flapjack/persistence/data_mapper/models/related_check.rb +3 -1
  68. data/lib/flapjack/pikelet.rb +56 -0
  69. data/lib/flapjack/transports/beanstalkd.rb +1 -1
  70. data/lib/flapjack/transports/result.rb +6 -6
  71. data/lib/flapjack/utility.rb +46 -0
  72. data/lib/flapjack/version.rb +5 -0
  73. data/lib/flapjack/web.rb +198 -0
  74. data/lib/flapjack/web/views/acknowledge.haml +55 -0
  75. data/lib/flapjack/web/views/check.haml +162 -0
  76. data/lib/flapjack/web/views/index.haml +92 -0
  77. data/lib/flapjack/web/views/self_stats.haml +56 -0
  78. data/lib/flapjack/{applications/worker.rb → worker/application.rb} +0 -0
  79. data/lib/flapjack/worker/cli.rb +49 -0
  80. data/{spec → spec.old}/check_sandbox/echo +0 -0
  81. data/{spec → spec.old}/check_sandbox/sandboxed_check +0 -0
  82. data/{spec → spec.old}/configs/flapjack-notifier-couchdb.ini +0 -0
  83. data/{spec → spec.old}/configs/flapjack-notifier.ini +0 -0
  84. data/{spec → spec.old}/configs/recipients.ini +0 -0
  85. data/{spec → spec.old}/helpers.rb +0 -0
  86. data/{spec → spec.old}/inifile_spec.rb +0 -0
  87. data/{spec → spec.old}/mock-notifiers/mock/init.rb +0 -0
  88. data/{spec → spec.old}/mock-notifiers/mock/mock.rb +0 -0
  89. data/{spec → spec.old}/notifier-directories/spoons/testmailer/init.rb +0 -0
  90. data/{spec → spec.old}/notifier_application_spec.rb +0 -0
  91. data/{spec → spec.old}/notifier_filters_spec.rb +0 -0
  92. data/{spec → spec.old}/notifier_options_multiplexer_spec.rb +0 -0
  93. data/{spec → spec.old}/notifier_options_spec.rb +0 -0
  94. data/{spec → spec.old}/notifier_spec.rb +0 -0
  95. data/{spec → spec.old}/notifiers/mailer_spec.rb +0 -0
  96. data/{spec → spec.old}/notifiers/xmpp_spec.rb +0 -0
  97. data/{spec → spec.old}/persistence/datamapper_spec.rb +0 -0
  98. data/{spec → spec.old}/persistence/mock_persistence_backend.rb +0 -0
  99. data/{spec → spec.old}/simple.ini +0 -0
  100. data/{spec → spec.old}/spec.opts +0 -0
  101. data/{spec → spec.old}/test-filters/blocker.rb +0 -0
  102. data/{spec → spec.old}/test-filters/mock.rb +0 -0
  103. data/{spec → spec.old}/transports/beanstalkd_spec.rb +0 -0
  104. data/{spec → spec.old}/transports/mock_transport.rb +0 -0
  105. data/{spec → spec.old}/worker_application_spec.rb +0 -0
  106. data/{spec → spec.old}/worker_options_spec.rb +0 -0
  107. data/spec/lib/flapjack/api/entity_check_presenter_spec.rb +117 -0
  108. data/spec/lib/flapjack/api/entity_presenter_spec.rb +92 -0
  109. data/spec/lib/flapjack/api_spec.rb +170 -0
  110. data/spec/lib/flapjack/coordinator_spec.rb +16 -0
  111. data/spec/lib/flapjack/data/entity_check_spec.rb +398 -0
  112. data/spec/lib/flapjack/data/entity_spec.rb +71 -0
  113. data/spec/lib/flapjack/data/event_spec.rb +6 -0
  114. data/spec/lib/flapjack/executive_spec.rb +59 -0
  115. data/spec/lib/flapjack/filters/acknowledgement_spec.rb +6 -0
  116. data/spec/lib/flapjack/filters/delays_spec.rb +6 -0
  117. data/spec/lib/flapjack/filters/detect_mass_client_failures_spec.rb +6 -0
  118. data/spec/lib/flapjack/filters/ok_spec.rb +6 -0
  119. data/spec/lib/flapjack/filters/scheduled_maintenance_spec.rb +6 -0
  120. data/spec/lib/flapjack/filters/unscheduled_maintenance_spec.rb +6 -0
  121. data/spec/lib/flapjack/jabber_spec.rb +150 -0
  122. data/spec/lib/flapjack/notification/email_spec.rb +6 -0
  123. data/spec/lib/flapjack/notification/sms_spec.rb +6 -0
  124. data/spec/lib/flapjack/pikelet_spec.rb +28 -0
  125. data/spec/lib/flapjack/web_spec.rb +188 -0
  126. data/spec/spec_helper.rb +44 -0
  127. data/spec/support/profile_all_formatter.rb +44 -0
  128. data/spec/support/uncolored_doc_formatter.rb +9 -0
  129. data/tasks/events.rake +85 -0
  130. data/tmp/acknowledge.rb +14 -0
  131. data/tmp/create_config_yaml.rb +16 -0
  132. data/tmp/create_events_failure.rb +33 -0
  133. data/tmp/create_events_ok.rb +33 -0
  134. data/tmp/create_events_ok_fail_ack_ok.rb +54 -0
  135. data/tmp/create_events_ok_failure.rb +40 -0
  136. data/tmp/create_events_ok_failure_ack.rb +54 -0
  137. data/tmp/dummy_entities.json +1 -0
  138. data/tmp/generate_nagios_test_hosts.rb +16 -0
  139. data/tmp/parse_config_yaml.rb +7 -0
  140. data/tmp/redis_delete_all_keys.rb +11 -0
  141. data/tmp/test_entities.json +1 -0
  142. metadata +482 -221
  143. data/TODO.md +0 -36
  144. data/VERSION +0 -1
  145. data/bin/flapjack-benchmark +0 -50
  146. data/bin/flapjack-notifier +0 -21
  147. data/bin/flapjack-notifier-manager +0 -43
  148. data/bin/flapjack-stats +0 -27
  149. data/bin/flapjack-worker +0 -13
  150. data/bin/flapjack-worker-manager +0 -35
  151. data/dist/etc/init.d/flapjack-notifier +0 -47
  152. data/dist/etc/init.d/flapjack-workers +0 -44
  153. data/features/flapjack-notifier-manager.feature +0 -19
  154. data/features/flapjack-worker-manager.feature +0 -27
  155. data/features/flapjack-worker.feature +0 -27
  156. data/features/netsaint-config-converter.feature +0 -126
  157. data/features/persistence/couch.feature +0 -105
  158. data/features/persistence/sqlite3.feature +0 -105
  159. data/features/persistence/steps/couch_steps.rb +0 -25
  160. data/features/persistence/steps/generic_steps.rb +0 -102
  161. data/features/persistence/steps/sqlite3_steps.rb +0 -13
  162. data/features/steps/flapjack-notifier-manager_steps.rb +0 -24
  163. data/features/steps/flapjack-worker-manager_steps.rb +0 -48
  164. data/lib/flapjack/applications/notifier.rb +0 -222
  165. data/lib/flapjack/cli/notifier.rb +0 -108
  166. data/lib/flapjack/cli/notifier_manager.rb +0 -86
  167. data/lib/flapjack/cli/worker.rb +0 -51
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ module Flapjack
4
+
5
+ module Data
6
+
7
+ class Contact
8
+
9
+ # takes a check, looks up contacts that are interested in this check (or in the check's entity)
10
+ # and returns an array of contact ids
11
+ def self.find_all_for_entity_check(entity_check, options = {})
12
+ logger = options[:logger]
13
+ logger = nil
14
+ raise "Redis connection not set" unless redis = options[:redis]
15
+
16
+ entity = entity_check.entity
17
+ check = entity_check.check
18
+
19
+ if logger
20
+ logger.debug("contacts for #{entity.id} (#{entity.name}): " + redis.smembers("contacts_for:#{entity.id}").length.to_s)
21
+ logger.debug("contacts for #{check}: " + redis.smembers("contacts_for:#{check}").length.to_s)
22
+ end
23
+
24
+ union = redis.sunion("contacts_for:#{entity.id}", "contacts_for:#{check}")
25
+ logger.debug("contacts for union of #{entity.id} and #{check}: " + union.length.to_s) if logger
26
+ union
27
+ end
28
+
29
+ def self.pagerduty_credentials_for_contact(contact, options = {})
30
+ logger = options[:logger]
31
+ raise "Redis connection not set" unless redis = options[:redis]
32
+
33
+ service_key = redis.hget("contact_media:#{contact}", 'pagerduty')
34
+ return nil unless service_key
35
+
36
+ deets = redis.hgetall("contact_pagerduty:#{contact}")
37
+ return deets.merge('service_key' => service_key)
38
+
39
+ end
40
+
41
+ end
42
+
43
+ end
44
+
45
+ end
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ module Flapjack
4
+
5
+ module Data
6
+
7
+ class Entity
8
+
9
+ attr_accessor :name, :id
10
+
11
+ def self.all(options = {})
12
+ raise "Redis connection not set" unless redis = options[:redis]
13
+ redis.keys("entity_id:*").collect {|k|
14
+ k =~ /^entity_id:(.+)$/; entity_name = $1
15
+ self.new(:name => entity_name, :id => redis.get("entity_id:#{entity_name}").to_i, :redis => redis)
16
+ }
17
+ end
18
+
19
+ def self.add(entity, options = {})
20
+ raise "Redis connection not set" unless redis = options[:redis]
21
+ raise "Entity name not provided" unless entity['name'] && !entity['name'].empty?
22
+
23
+ if entity['id']
24
+ redis.multi
25
+ existing_name = redis.hget("entity:#{entity['id']}", 'name')
26
+ redis.del("entity_id:#{existing_name}") unless existing_name == entity['name']
27
+ redis.set("entity_id:#{entity['name']}", entity['id'])
28
+ redis.hset("entity:#{entity['id']}", 'name', entity['name'])
29
+
30
+ redis.del("contacts_for:#{entity['id']}")
31
+ if entity['contacts'] && entity['contacts'].respond_to?(:each)
32
+ entity['contacts'].each {|contact|
33
+ redis.sadd("contacts_for:#{entity['id']}", contact)
34
+ }
35
+ end
36
+ redis.exec
37
+ else
38
+ # empty string is the redis equivalent of a Ruby nil, i.e. key with
39
+ # no value
40
+ redis.set("entity_id:#{entity['name']}", '')
41
+ end
42
+ end
43
+
44
+ def self.find_by_name(entity_name, options = {})
45
+ raise "Redis connection not set" unless redis = options[:redis]
46
+ entity_id = redis.get("entity_id:#{entity_name}")
47
+ if entity_id.nil?
48
+ # key doesn't exist
49
+ return unless options[:create]
50
+ self.add({'name' => entity_name}, :redis => redis)
51
+ end
52
+ self.new(:name => entity_name,
53
+ :id => (entity_id.nil? || entity_id.empty?) ? nil : entity_id.to_i,
54
+ :redis => redis)
55
+ end
56
+
57
+ def self.find_by_id(entity_id, options = {})
58
+ raise "Redis connection not set" unless redis = options[:redis]
59
+ entity_name = redis.hget("entity:#{entity_id}", 'name')
60
+ return if entity_name.nil? || entity_name.empty?
61
+ self.new(:name => entity_name, :id => entity_id, :redis => redis)
62
+ end
63
+
64
+ def check_list
65
+ @redis.keys("check:#{@name}:*").map {|k| k =~ /^check:#{@name}:(.+)$/; $1}
66
+ end
67
+
68
+ def check_count
69
+ checks = check_list
70
+ return if checks.nil?
71
+ checks.length
72
+ end
73
+
74
+ private
75
+
76
+ # NB: initializer should not be used directly -- instead one of the finder methods
77
+ # above will call it
78
+ def initialize(options = {})
79
+ raise "Redis connection not set" unless @redis = options[:redis]
80
+ raise "Entity name not set" unless @name = options[:name]
81
+ @id = options[:id]
82
+ @logger = options[:logger]
83
+ end
84
+
85
+ end
86
+
87
+ end
88
+
89
+ end
@@ -0,0 +1,396 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'yajl/json_gem'
4
+
5
+ require 'flapjack/patches'
6
+
7
+ require 'flapjack/data/entity'
8
+
9
+ module Flapjack
10
+
11
+ module Data
12
+
13
+ class EntityCheck
14
+
15
+ STATE_OK = 'ok'
16
+ STATE_WARNING = 'warning'
17
+ STATE_CRITICAL = 'critical'
18
+ STATE_UNKNOWN = 'unknown'
19
+
20
+ attr_accessor :entity, :check
21
+
22
+ # TODO probably shouldn't always be creating on query -- work out when this should be happening
23
+ def self.for_event_id(event_id, options = {})
24
+ raise "Redis connection not set" unless redis = options[:redis]
25
+ entity_name, check = event_id.split(':')
26
+ self.new(Flapjack::Data::Entity.find_by_name(entity_name, :redis => redis, :create => true), check,
27
+ :redis => redis)
28
+ end
29
+
30
+ # TODO probably shouldn't always be creating on query -- work out when this should be happening
31
+ def self.for_entity_name(entity_name, check, options = {})
32
+ raise "Redis connection not set" unless redis = options[:redis]
33
+ self.new(Flapjack::Data::Entity.find_by_name(entity_name, :redis => redis, :create => true), check,
34
+ :redis => redis)
35
+ end
36
+
37
+ def self.for_entity_id(entity_id, check, options = {})
38
+ raise "Redis connection not set" unless redis = options[:redis]
39
+ self.new(Flapjack::Data::Entity.find_by_id(entity_id, :redis => redis), check,
40
+ :redis => redis)
41
+ end
42
+
43
+ def self.for_entity(entity, check, options = {})
44
+ raise "Redis connection not set" unless redis = options[:redis]
45
+ self.new(entity, check, :redis => redis)
46
+ end
47
+
48
+ def entity_name
49
+ @entity.name
50
+ end
51
+
52
+ # takes a key "entity:check", returns true if the check is in unscheduled
53
+ # maintenance
54
+ def in_unscheduled_maintenance?
55
+ @redis.exists("#{@key}:unscheduled_maintenance")
56
+ end
57
+
58
+ # returns true if the check is in scheduled maintenance
59
+ def in_scheduled_maintenance?
60
+ @redis.exists("#{@key}:scheduled_maintenance")
61
+ end
62
+
63
+ # creates, or modifies, an event object and adds it to the events list in redis
64
+ # 'type' => 'service',
65
+ # 'state' => state,
66
+ # 'summary' => check_output,
67
+ # 'time' => timestamp
68
+ def create_event(event)
69
+ event.merge!('entity' => @entity.name, 'check' => @check)
70
+ event['time'] = Time.now.to_i if event['time'].nil?
71
+ @redis.rpush('events', Yajl::Encoder.encode(event))
72
+ end
73
+
74
+ def create_acknowledgement(opts = {})
75
+ defaults = {
76
+ 'summary' => '...'
77
+ }
78
+ options = defaults.merge(opts)
79
+
80
+ event = { 'type' => 'action',
81
+ 'state' => 'acknowledgement',
82
+ 'summary' => options['summary'],
83
+ 'duration' => options['duration'],
84
+ 'acknowledgement_id' => options['acknowledgement_id']
85
+ }
86
+ create_event(event)
87
+ end
88
+
89
+ # FIXME: need to add summary to summary of existing unscheduled maintenance if there is
90
+ # one, and extend duration / expiry time, instead of creating a separate unscheduled
91
+ # outage as we are doing now...
92
+ def create_unscheduled_maintenance(opts = {})
93
+ start_time = opts[:start_time] # unix timestamp
94
+ duration = opts[:duration] # seconds
95
+ summary = opts[:summary]
96
+ time_remaining = (start_time + duration) - Time.now.to_i
97
+ if time_remaining > 0
98
+ @redis.setex("#{@key}:unscheduled_maintenance", time_remaining, start_time)
99
+ end
100
+ @redis.zadd("#{@key}:unscheduled_maintenances", duration, start_time)
101
+ @redis.set("#{@key}:#{start_time}:unscheduled_maintenance:summary", summary)
102
+
103
+ @redis.zadd("#{@key}:sorted_unscheduled_maintenance_timestamps", start_time, start_time)
104
+ end
105
+
106
+ # ends any unscheduled maintenance
107
+ def end_unscheduled_maintenance(opts = {})
108
+ defaults = {
109
+ :end_time => Time.now.to_i
110
+ }
111
+ options = defaults.merge(opts)
112
+ end_time = options[:end_time]
113
+
114
+ if (um_start = @redis.get("#{@key}:unscheduled_maintenance"))
115
+ duration = end_time - um_start.to_i
116
+ @logger.debug("ending unscheduled downtime for #{@key} at #{Time.at(end_time).to_s}") if @logger
117
+ @redis.del("#{@key}:unscheduled_maintenance")
118
+ @redis.zadd("#{@key}:unscheduled_maintenances", duration, um_start)
119
+ @redis.zadd("#{@key}:sorted_unscheduled_maintenance_timestamps", um_start, um_start)
120
+ else
121
+ @logger.debug("end_unscheduled_maintenance called for #{@key} but none found") if @logger
122
+ end
123
+ end
124
+
125
+ # creates a scheduled maintenance period for a check
126
+ # TODO: consider adding some validation to the data we're adding in here
127
+ # eg start_time is a believable unix timestamp (not in the past and not too
128
+ # far in the future), duration is within some bounds...
129
+ def create_scheduled_maintenance(opts = {})
130
+ start_time = opts[:start_time] # unix timestamp
131
+ duration = opts[:duration] # seconds
132
+ summary = opts[:summary]
133
+ @redis.zadd("#{@key}:scheduled_maintenances", duration, start_time)
134
+ @redis.set("#{@key}:#{start_time}:scheduled_maintenance:summary", summary)
135
+
136
+ @redis.zadd("#{@key}:sorted_scheduled_maintenance_timestamps", start_time, start_time)
137
+
138
+ # scheduled maintenance periods have changed, revalidate
139
+ update_scheduled_maintenance(:revalidate => true)
140
+ end
141
+
142
+ # delete a scheduled maintenance
143
+ def delete_scheduled_maintenance(opts = {})
144
+ start_time = opts[:start_time]
145
+ @redis.del("#{@key}:#{start_time}:scheduled_maintenance:summary")
146
+ @redis.zrem("#{@key}:scheduled_maintenances", start_time)
147
+
148
+ @redis.zremrangebyscore("#{@key}:sorted_scheduled_maintenance_timestamps", start_time, start_time)
149
+
150
+ # scheduled maintenance periods have changed, revalidate
151
+ update_scheduled_maintenance(:revalidate => true)
152
+ end
153
+
154
+ # if not in scheduled maintenance, looks in scheduled maintenance list for a check to see if
155
+ # current state should be set to scheduled maintenance, and sets it as appropriate
156
+ def update_scheduled_maintenance(opts = {})
157
+ if opts[:revalidate]
158
+ @redis.del("#{@key}:scheduled_maintenance")
159
+ else
160
+ return if in_scheduled_maintenance?
161
+ end
162
+
163
+ # are we within a scheduled maintenance period?
164
+ t = Time.now.to_i
165
+ current_sched_ms = maintenances(nil, nil, :scheduled => true).select {|sm|
166
+ (sm[:start_time] <= t) && (t < sm[:end_time])
167
+ }
168
+ return if current_sched_ms.empty?
169
+
170
+ # yes! so set current scheduled maintenance
171
+ # if multiple scheduled maintenances found, find the end_time furthest in the future
172
+ most_futuristic = current_sched_ms.max {|sm| sm[:end_time] }
173
+ start_time = most_futuristic[:start_time]
174
+ duration = most_futuristic[:duration]
175
+ @redis.setex("#{@key}:scheduled_maintenance", duration.to_i, start_time)
176
+ end
177
+
178
+ # returns nil if no previous state; this must be considered as a possible
179
+ # state by classes using this model
180
+ def state
181
+ @redis.hget("check:#{@key}", 'state')
182
+ end
183
+
184
+ def update_state(state, options = {})
185
+ return unless validate_state(state)
186
+ timestamp = options[:timestamp] || Time.now.to_i
187
+ client = options[:client]
188
+ summary = options[:summary]
189
+ count = options[:count]
190
+
191
+ # Note the current state (for speedy lookups)
192
+ @redis.hset("check:#{@key}", 'state', state)
193
+
194
+ # FIXME: rename to last_state_change?
195
+ @redis.hset("check:#{@key}", 'last_change', timestamp)
196
+
197
+ # Retain all state changes for entity:check pair
198
+ @redis.rpush("#{@key}:states", timestamp)
199
+ @redis.set("#{@key}:#{timestamp}:state", state)
200
+ @redis.set("#{@key}:#{timestamp}:summary", summary) if summary
201
+ @redis.set("#{@key}:#{timestamp}:count", count) if count
202
+
203
+ @redis.zadd("#{@key}:sorted_state_timestamps", timestamp, timestamp)
204
+
205
+ case state
206
+ when STATE_WARNING, STATE_CRITICAL
207
+ @redis.zadd('failed_checks', timestamp, @key)
208
+ # FIXME: Iterate through a list of tags associated with an entity:check pair, and update counters
209
+ @redis.zadd("failed_checks:client:#{client}", timestamp, @key) if client
210
+ else
211
+ @redis.zrem("failed_checks", @key)
212
+ # FIXME: Iterate through a list of tags associated with an entity:check pair, and update counters
213
+ @redis.zrem("failed_checks:client:#{client}", @key) if client
214
+ end
215
+ end
216
+
217
+ def last_update
218
+ lu = @redis.hget("check:#{@key}", 'last_update')
219
+ return unless (lu && lu =~ /^\d+$/)
220
+ lu.to_i
221
+ end
222
+
223
+ def last_update=(timestamp)
224
+ @redis.hset("check:#{@key}", 'last_update', timestamp)
225
+ end
226
+
227
+ def last_change
228
+ lc = @redis.hget("check:#{@key}", 'last_change')
229
+ return unless (lc && lc =~ /^\d+$/)
230
+ lc.to_i
231
+ end
232
+
233
+ def last_problem_notification
234
+ lpn = @redis.get("#{@key}:last_problem_notification")
235
+ return unless (lpn && lpn =~ /^\d+$/)
236
+ lpn.to_i
237
+ end
238
+
239
+ def last_recovery_notification
240
+ lrn = @redis.get("#{@key}:last_recovery_notification")
241
+ return unless (lrn && lrn =~ /^\d+$/)
242
+ lrn.to_i
243
+ end
244
+
245
+ def last_acknowledgement_notification
246
+ lan = @redis.get("#{@key}:last_acknowledgement_notification")
247
+ return unless (lan && lan =~ /^\d+$/)
248
+ lan.to_i
249
+ end
250
+
251
+ def event_count_at(timestamp)
252
+ eca = @redis.get("#{@key}:#{timestamp}:count")
253
+ return unless (eca && eca =~ /^\d+$/)
254
+ eca.to_i
255
+ end
256
+
257
+ def failed?
258
+ [STATE_WARNING, STATE_CRITICAL].include?( state )
259
+ end
260
+
261
+ def ok?
262
+ [STATE_OK].include?( state )
263
+ end
264
+
265
+ def summary
266
+ timestamp = @redis.lindex("#{@key}:states", -1)
267
+ @redis.get("#{@key}:#{timestamp}:summary")
268
+ end
269
+
270
+ # Returns a list of states for this entity check, sorted by timestamp.
271
+ #
272
+ # start_time and end_time should be passed as integer timestamps; these timestamps
273
+ # will be considered inclusively, so, e.g. coverage for a day should go
274
+ # from midnight to 11:59:59 PM. Pass nil for either end to leave that
275
+ # side unbounded.
276
+ def historical_states(start_time, end_time, opts = {})
277
+ start_time ||= '-inf'
278
+ end_time ||= '+inf'
279
+ order = opts[:order]
280
+ query = (order && 'desc'.eql?(order.downcase)) ? :zrevrangebyscore : :zrangebyscore
281
+ state_ts = @redis.send(query, "#{@key}:sorted_state_timestamps", start_time, end_time)
282
+
283
+ state_data = nil
284
+
285
+ @redis.multi do |r|
286
+ state_data = state_ts.collect {|ts|
287
+ {:timestamp => ts.to_i,
288
+ :state => r.get("#{@key}:#{ts}:state"),
289
+ :summary => r.get("#{@key}:#{ts}:summary")}
290
+ }
291
+ end
292
+
293
+ # The redis commands in a pipeline block return future objects, which
294
+ # must be evaluated. This relies on a patch in flapjack/patches.rb to
295
+ # make the Future objects report their class.
296
+ state_data.collect {|sd|
297
+ sd.merge!(sd) {|k,ov,nv|
298
+ (nv.class == Redis::Future) ? nv.value : nv
299
+ }
300
+ }
301
+ end
302
+
303
+ # requires a known state timestamp, i.e. probably one returned via
304
+ # historical_states. will find the one before that in the sorted set,
305
+ # if any.
306
+ def historical_state_before(timestamp)
307
+ pos = @redis.zrank("#{@key}:sorted_state_timestamps", timestamp)
308
+ return if pos < 1
309
+ ts = @redis.zrange("#{@key}:sorted_state_timestamps", pos - 1, pos)
310
+ return if ts.nil? || ts.empty?
311
+ {:timestamp => ts.first.to_i,
312
+ :state => @redis.get("#{@key}:#{ts.first}:state"),
313
+ :summary => @redis.get("#{@key}:#{ts.first}:summary")}
314
+ end
315
+
316
+ def historical_state_after(timestamp)
317
+ pos = @redis.zrank("#{@key}:sorted_state_timestamps", timestamp)
318
+ ts = @redis.zrange("#{@key}:sorted_state_timestamps", pos + 1, pos + 2)
319
+ return if ts.nil? || ts.empty?
320
+ {:timestamp => ts.first.to_i,
321
+ :state => @redis.get("#{@key}:#{ts.first}:state"),
322
+ :summary => @redis.get("#{@key}:#{ts.first}:summary")}
323
+ end
324
+
325
+ # Returns a list of maintenance periods (either unscheduled or scheduled) for this
326
+ # entity check, sorted by timestamp.
327
+ #
328
+ # start_time and end_time should be passed as integer timestamps; these timestamps
329
+ # will be considered inclusively, so, e.g. coverage for a day should go
330
+ # from midnight to 11:59:59 PM. Pass nil for either end to leave that
331
+ # side unbounded.
332
+ def maintenances(start_time, end_time, opts = {})
333
+ sched = opts[:scheduled] ? 'scheduled' : 'unscheduled'
334
+
335
+ start_time ||= '-inf'
336
+ end_time ||= '+inf'
337
+ order = opts[:order]
338
+ query = (order && 'desc'.eql?(order.downcase)) ? :zrevrangebyscore : :zrangebyscore
339
+ maint_ts = @redis.send(query, "#{@key}:sorted_#{sched}_maintenance_timestamps", start_time, end_time)
340
+
341
+ maint_data = nil
342
+
343
+ @redis.multi do |r|
344
+ maint_data = maint_ts.collect {|ts|
345
+ {:start_time => ts.to_i,
346
+ :duration => r.zscore("#{@key}:#{sched}_maintenances", ts),
347
+ :summary => r.get("#{@key}:#{ts}:#{sched}_maintenance:summary"),
348
+ }
349
+ }
350
+ end
351
+
352
+ # The redis commands in a pipeline block return future objects, which
353
+ # must be evaluated. This relies on a patch in flapjack/patches.rb to
354
+ # make the Future objects report their class.
355
+ maint_data.collect {|md|
356
+ md.merge!(md) {|k,ov,nv| (nv.class == Redis::Future) ? nv.value : nv }
357
+ md[:end_time] = (md[:start_time] + md[:duration]).floor
358
+ md
359
+ }
360
+ end
361
+
362
+ # returns an array of pagerduty credentials. If more than one contact for this entity_check
363
+ # has pagerduty credentials then there'll be one hash in the array for each set of
364
+ # credentials.
365
+ def pagerduty_credentials(options)
366
+ creds = []
367
+ raise "Redis connection not set" unless redis = options[:redis]
368
+ logger = options[:logger]
369
+
370
+ contacts = Flapjack::Data::Contact.find_all_for_entity_check(self, { :redis => redis, :logger => logger })
371
+ contacts.each {|contact|
372
+ cred = Flapjack::Data::Contact.pagerduty_credentials_for_contact(contact, { :redis => redis, :logger => logger })
373
+ creds << cred if cred
374
+ }
375
+ creds
376
+ end
377
+
378
+ private
379
+
380
+ # Passing around the redis handle like this is a SMELL.
381
+ def initialize(entity, check, options = {})
382
+ raise "Redis connection not set" unless @redis = options[:redis]
383
+ raise "Invalid entity" unless @entity = entity
384
+ raise "Invalid check" unless @check = check
385
+ @key = "#{entity.name}:#{check}"
386
+ end
387
+
388
+ def validate_state(state)
389
+ [STATE_OK, STATE_WARNING, STATE_CRITICAL, STATE_UNKNOWN].include?(state)
390
+ end
391
+
392
+ end
393
+
394
+ end
395
+
396
+ end