flapjack 0.6.61 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/Gemfile +2 -1
- data/README.md +8 -4
- data/features/events.feature +269 -146
- data/features/notification_rules.feature +93 -0
- data/features/steps/events_steps.rb +162 -21
- data/features/steps/notifications_steps.rb +1 -1
- data/features/steps/time_travel_steps.rb +30 -19
- data/features/support/env.rb +71 -1
- data/flapjack.gemspec +3 -0
- data/lib/flapjack/data/contact.rb +256 -57
- data/lib/flapjack/data/entity.rb +2 -1
- data/lib/flapjack/data/entity_check.rb +22 -7
- data/lib/flapjack/data/global.rb +1 -0
- data/lib/flapjack/data/message.rb +2 -0
- data/lib/flapjack/data/notification_rule.rb +172 -0
- data/lib/flapjack/data/tag.rb +7 -2
- data/lib/flapjack/data/tag_set.rb +16 -0
- data/lib/flapjack/executive.rb +147 -13
- data/lib/flapjack/filters/delays.rb +21 -9
- data/lib/flapjack/gateways/api.rb +407 -27
- data/lib/flapjack/gateways/pagerduty.rb +1 -1
- data/lib/flapjack/gateways/web.rb +50 -22
- data/lib/flapjack/gateways/web/views/self_stats.haml +2 -0
- data/lib/flapjack/utility.rb +10 -0
- data/lib/flapjack/version.rb +1 -1
- data/spec/lib/flapjack/data/contact_spec.rb +103 -6
- data/spec/lib/flapjack/data/global_spec.rb +2 -0
- data/spec/lib/flapjack/data/message_spec.rb +6 -0
- data/spec/lib/flapjack/data/notification_rule_spec.rb +22 -0
- data/spec/lib/flapjack/data/notification_spec.rb +6 -0
- data/spec/lib/flapjack/gateways/api_spec.rb +727 -4
- data/spec/lib/flapjack/gateways/jabber_spec.rb +1 -0
- data/spec/lib/flapjack/gateways/web_spec.rb +11 -1
- data/spec/spec_helper.rb +10 -0
- data/tmp/notification_rules.rb +73 -0
- data/tmp/test_json_post.rb +16 -0
- data/tmp/test_notification_rules_api.rb +170 -0
- metadata +59 -2
data/flapjack.gemspec
CHANGED
@@ -35,6 +35,9 @@ Gem::Specification.new do |gem|
|
|
35
35
|
gem.add_dependency 'blather', '0.8.1'
|
36
36
|
gem.add_dependency 'chronic'
|
37
37
|
gem.add_dependency 'chronic_duration'
|
38
|
+
gem.add_dependency 'activesupport'
|
39
|
+
gem.add_dependency 'ice_cube'
|
40
|
+
gem.add_dependency 'tzinfo'
|
38
41
|
|
39
42
|
gem.add_development_dependency 'rake'
|
40
43
|
end
|
@@ -4,8 +4,11 @@
|
|
4
4
|
# structure to avoid the need for this type of query
|
5
5
|
|
6
6
|
require 'set'
|
7
|
-
|
7
|
+
require 'ice_cube'
|
8
8
|
require 'flapjack/data/entity'
|
9
|
+
require 'flapjack/data/notification_rule'
|
10
|
+
require 'flapjack/data/tag'
|
11
|
+
require 'flapjack/data/tag_set'
|
9
12
|
|
10
13
|
module Flapjack
|
11
14
|
|
@@ -13,14 +16,14 @@ module Flapjack
|
|
13
16
|
|
14
17
|
class Contact
|
15
18
|
|
16
|
-
attr_accessor :first_name, :last_name, :email, :media, :pagerduty_credentials
|
19
|
+
attr_accessor :id, :first_name, :last_name, :email, :media, :pagerduty_credentials
|
20
|
+
|
21
|
+
TAG_PREFIX = 'contact_tag'
|
17
22
|
|
18
23
|
def self.all(options = {})
|
19
24
|
raise "Redis connection not set" unless redis = options[:redis]
|
20
25
|
|
21
|
-
|
22
|
-
|
23
|
-
contact_keys.inject([]) {|ret, k|
|
26
|
+
redis.keys('contact:*').inject([]) {|ret, k|
|
24
27
|
k =~ /^contact:(\d+)$/
|
25
28
|
id = $1
|
26
29
|
contact = self.find_by_id(id, :redis => redis)
|
@@ -29,66 +32,88 @@ module Flapjack
|
|
29
32
|
}.sort_by {|c| [c.last_name, c.first_name]}
|
30
33
|
end
|
31
34
|
|
32
|
-
def self.
|
35
|
+
def self.find_by_id(contact_id, options = {})
|
33
36
|
raise "Redis connection not set" unless redis = options[:redis]
|
37
|
+
raise "No id value passed" unless contact_id
|
38
|
+
logger = options[:logger]
|
34
39
|
|
35
|
-
|
36
|
-
|
37
|
-
# FIXME: when we do source tagging we can properly
|
38
|
-
# clean up contacts_for: keys
|
39
|
-
# redis.keys('contacts_for:*') +
|
40
|
-
redis.keys("contact_pagerduty:*")
|
40
|
+
# sanity check
|
41
|
+
return unless redis.hexists("contact:#{contact_id}", 'first_name')
|
41
42
|
|
42
|
-
|
43
|
+
contact = self.new(:id => contact_id, :redis => redis)
|
44
|
+
contact.refresh
|
45
|
+
contact
|
43
46
|
end
|
44
47
|
|
45
|
-
def self.
|
48
|
+
def self.add(contact_data, options = {})
|
46
49
|
raise "Redis connection not set" unless redis = options[:redis]
|
47
|
-
|
48
|
-
|
50
|
+
contact_id = contact_data['id']
|
51
|
+
raise "Contact id value not provided" if contact_id.nil?
|
49
52
|
|
50
|
-
|
53
|
+
if contact = self.find_by_id(contact_id, :redis => redis)
|
54
|
+
contact.delete!
|
55
|
+
end
|
56
|
+
|
57
|
+
self.add_or_update(contact_id, contact_data, :redis => redis)
|
58
|
+
self.find_by_id(contact_id, :redis => redis)
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.delete_all(options = {})
|
62
|
+
raise "Redis connection not set" unless redis = options[:redis]
|
51
63
|
|
52
|
-
|
53
|
-
|
64
|
+
self.all(:redis => redis).each do |contact|
|
65
|
+
contact.delete!
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# ensure that instance variables match redis state
|
70
|
+
# TODO may want to make this protected/private, it's only
|
71
|
+
# used in this class
|
72
|
+
def refresh
|
73
|
+
self.first_name, self.last_name, self.email =
|
74
|
+
@redis.hmget("contact:#{@id}", 'first_name', 'last_name', 'email')
|
75
|
+
self.media = @redis.hgetall("contact_media:#{@id}")
|
54
76
|
|
55
77
|
# similar to code in instance method pagerduty_credentials
|
56
|
-
|
57
|
-
|
58
|
-
|
78
|
+
if service_key = @redis.hget("contact_media:#{@id}", 'pagerduty')
|
79
|
+
self.pagerduty_credentials =
|
80
|
+
@redis.hgetall("contact_pagerduty:#{@id}").merge('service_key' => service_key)
|
59
81
|
end
|
82
|
+
end
|
60
83
|
|
61
|
-
|
62
|
-
|
84
|
+
def update(contact_data)
|
85
|
+
self.class.add_or_update(@id, contact_data, :redis => @redis)
|
86
|
+
self.refresh
|
63
87
|
end
|
64
88
|
|
89
|
+
def delete!
|
90
|
+
# remove entity & check registrations -- ugh, this will be slow.
|
91
|
+
# rather than check if the key is present we'll just request its
|
92
|
+
# deletion anyway, fewer round-trips
|
93
|
+
@redis.keys('contacts_for:*').each do |cfk|
|
94
|
+
@redis.srem(cfk, self.id)
|
95
|
+
end
|
65
96
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
# TODO maybe return the instantiated Contact record?
|
70
|
-
def self.add(contact, options = {})
|
71
|
-
raise "Redis connection not set" unless redis = options[:redis]
|
97
|
+
@redis.del("drop_alerts_for_contact:#{self.id}")
|
98
|
+
dafc = @redis.keys("drop_alerts_for_contact:#{self.id}:*")
|
99
|
+
@redis.del(*dafc) unless dafc.empty?
|
72
100
|
|
73
|
-
|
74
|
-
|
75
|
-
|
101
|
+
# TODO if implemented, alerts_by_contact & alerts_by_check:
|
102
|
+
# list all alerts from all matched keys, remove them from
|
103
|
+
# the main alerts sorted set, remove all alerts_by sorted sets
|
104
|
+
# for the contact
|
76
105
|
|
77
|
-
|
78
|
-
|
106
|
+
# remove this contact from all tags it's marked with
|
107
|
+
self.delete_tags(*self.tags.to_a)
|
79
108
|
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
when 'pagerduty'
|
84
|
-
redis.hset("contact_media:#{contact['id']}", medium, address['service_key'])
|
85
|
-
redis.hmset("contact_pagerduty:#{contact['id']}",
|
86
|
-
*['subdomain', 'username', 'password'].collect {|f| [f, address[f]]})
|
87
|
-
else
|
88
|
-
redis.hset("contact_media:#{contact['id']}", medium, address)
|
89
|
-
end
|
90
|
-
}
|
109
|
+
# remove all associated notification rules
|
110
|
+
self.notification_rules.each do |nr|
|
111
|
+
self.delete_notification_rule(nr)
|
91
112
|
end
|
113
|
+
|
114
|
+
@redis.del("contact:#{self.id}", "contact_media:#{self.id}",
|
115
|
+
"contact_media_intervals:#{self.id}",
|
116
|
+
"contact_tz:#{self.id}", "contact_pagerduty:#{self.id}")
|
92
117
|
end
|
93
118
|
|
94
119
|
def pagerduty_credentials
|
@@ -97,7 +122,9 @@ module Flapjack
|
|
97
122
|
merge('service_key' => service_key)
|
98
123
|
end
|
99
124
|
|
100
|
-
|
125
|
+
# NB ideally contacts_for:* keys would scope the entity and check by an
|
126
|
+
# input source, for namespacing purposes
|
127
|
+
def entities(options = {})
|
101
128
|
@redis.keys('contacts_for:*').inject({}) {|ret, k|
|
102
129
|
if @redis.sismember(k, self.id)
|
103
130
|
if k =~ /^contacts_for:([a-zA-Z0-9][a-zA-Z0-9\.\-]*[a-zA-Z0-9])(?::(\w+))?$/
|
@@ -111,16 +138,20 @@ module Flapjack
|
|
111
138
|
:id => entity_id, :redis => @redis)
|
112
139
|
ret[entity_id][:entity] = entity
|
113
140
|
end
|
114
|
-
|
115
|
-
|
141
|
+
if options[:checks]
|
142
|
+
# using a set to ensure unique check values
|
143
|
+
ret[entity_id][:checks] = Set.new
|
144
|
+
end
|
116
145
|
end
|
117
146
|
|
118
|
-
if
|
119
|
-
#
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
147
|
+
if options[:checks]
|
148
|
+
# if not registered for the check, then was registered for
|
149
|
+
# the entity, so add all checks
|
150
|
+
ret[entity_id][:checks] |= (check || entity.check_list)
|
151
|
+
end
|
152
|
+
|
153
|
+
if options[:tags]
|
154
|
+
ret[entity_id][:tags] = entity.tags
|
124
155
|
end
|
125
156
|
end
|
126
157
|
end
|
@@ -132,12 +163,180 @@ module Flapjack
|
|
132
163
|
[(self.first_name || ''), (self.last_name || '')].join(" ").strip
|
133
164
|
end
|
134
165
|
|
166
|
+
# return an array of the notification rules of this contact
|
167
|
+
def notification_rules
|
168
|
+
@redis.smembers("contact_notification_rules:#{self.id}").collect { |rule_id|
|
169
|
+
next if (rule_id.nil? || rule_id == '')
|
170
|
+
Flapjack::Data::NotificationRule.find_by_id(rule_id, {:redis => @redis })
|
171
|
+
}.compact
|
172
|
+
end
|
173
|
+
|
174
|
+
def add_notification_rule(rule_data)
|
175
|
+
Flapjack::Data::NotificationRule.add(rule_data.merge(:contact_id => self.id),
|
176
|
+
:redis => @redis)
|
177
|
+
end
|
178
|
+
|
179
|
+
def delete_notification_rule(rule)
|
180
|
+
@redis.srem("contact_notification_rules:#{self.id}", rule.id)
|
181
|
+
@redis.del("notification_rule:#{rule.id}")
|
182
|
+
end
|
183
|
+
|
184
|
+
def media_intervals
|
185
|
+
@redis.hgetall("contact_media_intervals:#{self.id}")
|
186
|
+
end
|
187
|
+
|
188
|
+
# how often to notify this contact on the given media
|
189
|
+
# return 15 mins if no value is set
|
190
|
+
def interval_for_media(media)
|
191
|
+
interval = @redis.hget("contact_media_intervals:#{self.id}", media)
|
192
|
+
(interval.nil? || (interval.to_i <= 0)) ? (15 * 60) : interval.to_i
|
193
|
+
end
|
194
|
+
|
195
|
+
def set_interval_for_media(media, interval)
|
196
|
+
raise "invalid interval" unless interval.is_a?(Integer)
|
197
|
+
@redis.hset("contact_media_intervals:#{self.id}", media, interval)
|
198
|
+
end
|
199
|
+
|
200
|
+
def set_address_for_media(media, address)
|
201
|
+
@redis.hset("contact_media:#{self.id}", media, address)
|
202
|
+
if media == 'pagerduty'
|
203
|
+
# FIXME - work out what to do when changing the pagerduty service key (address)
|
204
|
+
# probably best solution is to remove the need to have the username and password
|
205
|
+
# and subdomain as pagerduty's updated api's mean we don't them anymore I think...
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
def remove_media(media)
|
210
|
+
@redis.hdel("contact_media:#{self.id}", media)
|
211
|
+
@redis.hdel("contact_media_intervals:#{self.id}", media)
|
212
|
+
if media == 'pagerduty'
|
213
|
+
@redis.del("contact_pagerduty:#{self.id}")
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
# drop notifications for
|
218
|
+
def drop_notifications?(opts)
|
219
|
+
media = opts[:media]
|
220
|
+
check = opts[:check]
|
221
|
+
state = opts[:state]
|
222
|
+
# build it and they will come
|
223
|
+
@redis.exists("drop_alerts_for_contact:#{self.id}") ||
|
224
|
+
(media && @redis.exists("drop_alerts_for_contact:#{self.id}:#{media}")) ||
|
225
|
+
(media && check &&
|
226
|
+
@redis.exists("drop_alerts_for_contact:#{self.id}:#{media}:#{check}")) ||
|
227
|
+
(media && check && state &&
|
228
|
+
@redis.exists("drop_alerts_for_contact:#{self.id}:#{media}:#{check}:#{state}"))
|
229
|
+
end
|
230
|
+
|
231
|
+
def update_sent_alert_keys(opts)
|
232
|
+
media = opts[:media]
|
233
|
+
check = opts[:check]
|
234
|
+
state = opts[:state]
|
235
|
+
key = "drop_alerts_for_contact:#{self.id}:#{media}:#{check}:#{state}"
|
236
|
+
@redis.set(key, 'd')
|
237
|
+
@redis.expire(key, self.interval_for_media(media))
|
238
|
+
end
|
239
|
+
|
240
|
+
# FIXME
|
241
|
+
# do a mixin with the following tag methods, they will be the same
|
242
|
+
# across all objects we allow tags on
|
243
|
+
|
244
|
+
# return the set of tags for this contact
|
245
|
+
def tags
|
246
|
+
@tags ||= Flapjack::Data::TagSet.new( @redis.keys("#{TAG_PREFIX}:*").inject([]) {|memo, tag|
|
247
|
+
if Flapjack::Data::Tag.find(tag, :redis => @redis).include?(@id.to_s)
|
248
|
+
memo << tag.sub(/^#{TAG_PREFIX}:/, '')
|
249
|
+
end
|
250
|
+
memo
|
251
|
+
} )
|
252
|
+
end
|
253
|
+
|
254
|
+
# adds tags to this contact
|
255
|
+
def add_tags(*enum)
|
256
|
+
enum.each do |t|
|
257
|
+
Flapjack::Data::Tag.create("#{TAG_PREFIX}:#{t}", [@id], :redis => @redis)
|
258
|
+
tags.add(t)
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
# removes tags from this contact
|
263
|
+
def delete_tags(*enum)
|
264
|
+
enum.each do |t|
|
265
|
+
tag = Flapjack::Data::Tag.find("#{TAG_PREFIX}:#{t}", :redis => @redis)
|
266
|
+
tag.delete(@id)
|
267
|
+
tags.delete(t)
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
# return a list of media enabled for this contact
|
272
|
+
# eg [ 'email', 'sms' ]
|
273
|
+
def media_list
|
274
|
+
@redis.hkeys("contact_media:#{self.id}")
|
275
|
+
end
|
276
|
+
|
277
|
+
# return the timezone of the contact, or the system default if none is set
|
278
|
+
def timezone(opts = {})
|
279
|
+
tz_string = @redis.get("contact_tz:#{self.id}")
|
280
|
+
tz = opts[:default] if (tz_string.nil? || tz_string.empty?)
|
281
|
+
|
282
|
+
if tz.nil?
|
283
|
+
begin
|
284
|
+
tz = ActiveSupport::TimeZone.new(tz_string)
|
285
|
+
rescue ArgumentError
|
286
|
+
logger.warn("Invalid timezone string set for contact #{self.id} or TZ (#{tz_string}), using 'UTC'!")
|
287
|
+
tz = ActiveSupport::TimeZone.new('UTC')
|
288
|
+
end
|
289
|
+
end
|
290
|
+
tz
|
291
|
+
end
|
292
|
+
|
293
|
+
# sets or removes the timezone for the contact
|
294
|
+
def timezone=(tz)
|
295
|
+
if tz.nil?
|
296
|
+
@redis.del("contact_tz:#{self.id}")
|
297
|
+
else
|
298
|
+
# ActiveSupport::TimeZone or String
|
299
|
+
@redis.set("contact_tz:#{self.id}",
|
300
|
+
tz.respond_to?(:name) ? tz.name : tz )
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
304
|
+
def to_json(*args)
|
305
|
+
{ "id" => self.id,
|
306
|
+
"first_name" => self.first_name,
|
307
|
+
"last_name" => self.last_name,
|
308
|
+
"email" => self.email,
|
309
|
+
"tags" => self.tags.to_a }.to_json
|
310
|
+
end
|
311
|
+
|
135
312
|
private
|
136
313
|
|
137
314
|
def initialize(options = {})
|
138
315
|
raise "Redis connection not set" unless @redis = options[:redis]
|
139
|
-
|
140
|
-
|
316
|
+
@id = options[:id]
|
317
|
+
end
|
318
|
+
|
319
|
+
# NB: should probably be called in the context of a Redis multi block; not doing so
|
320
|
+
# here as calling classes may well be adding/updating multiple records in the one
|
321
|
+
# operation
|
322
|
+
def self.add_or_update(contact_id, contact_data, options = {})
|
323
|
+
raise "Redis connection not set" unless redis = options[:redis]
|
324
|
+
|
325
|
+
# TODO check that the rest of this is safe for the update case
|
326
|
+
redis.hmset("contact:#{contact_id}",
|
327
|
+
*['first_name', 'last_name', 'email'].collect {|f| [f, contact_data[f]]})
|
328
|
+
|
329
|
+
unless contact_data['media'].nil?
|
330
|
+
contact_data['media'].each_pair {|medium, address|
|
331
|
+
case medium
|
332
|
+
when 'pagerduty'
|
333
|
+
redis.hset("contact_media:#{contact_id}", medium, address['service_key'])
|
334
|
+
redis.hmset("contact_pagerduty:#{contact_id}",
|
335
|
+
*['subdomain', 'username', 'password'].collect {|f| [f, address[f]]})
|
336
|
+
else
|
337
|
+
redis.hset("contact_media:#{contact_id}", medium, address)
|
338
|
+
end
|
339
|
+
}
|
141
340
|
end
|
142
341
|
end
|
143
342
|
|
data/lib/flapjack/data/entity.rb
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
require 'flapjack/data/contact'
|
4
4
|
require 'flapjack/data/tag'
|
5
|
+
require 'flapjack/data/tag_set'
|
5
6
|
|
6
7
|
module Flapjack
|
7
8
|
|
@@ -110,7 +111,7 @@ module Flapjack
|
|
110
111
|
end
|
111
112
|
|
112
113
|
def tags
|
113
|
-
@tags ||= ::
|
114
|
+
@tags ||= Flapjack::Data::TagSet.new( @redis.keys("#{TAG_PREFIX}:*").inject([]) {|memo, entity_tag|
|
114
115
|
if Flapjack::Data::Tag.find(entity_tag, :redis => @redis).include?(@id.to_s)
|
115
116
|
memo << entity_tag.sub(/^#{TAG_PREFIX}:/, '')
|
116
117
|
end
|
@@ -271,6 +271,18 @@ module Flapjack
|
|
271
271
|
lpn.to_i
|
272
272
|
end
|
273
273
|
|
274
|
+
def last_warning_notification
|
275
|
+
lwn = @redis.get("#{@key}:last_warning_notification")
|
276
|
+
return unless (lwn && lwn =~ /^\d+$/)
|
277
|
+
lwn.to_i
|
278
|
+
end
|
279
|
+
|
280
|
+
def last_critical_notification
|
281
|
+
lcn = @redis.get("#{@key}:last_critical_notification")
|
282
|
+
return unless (lcn && lcn =~ /^\d+$/)
|
283
|
+
lcn.to_i
|
284
|
+
end
|
285
|
+
|
274
286
|
def last_recovery_notification
|
275
287
|
lrn = @redis.get("#{@key}:last_recovery_notification")
|
276
288
|
return unless (lrn && lrn =~ /^\d+$/)
|
@@ -285,6 +297,8 @@ module Flapjack
|
|
285
297
|
|
286
298
|
def last_notifications_of_each_type
|
287
299
|
ln = {:problem => last_problem_notification,
|
300
|
+
:warning => last_warning_notification,
|
301
|
+
:critical => last_critical_notification,
|
288
302
|
:recovery => last_recovery_notification,
|
289
303
|
:acknowledgement => last_acknowledgement_notification }
|
290
304
|
ln
|
@@ -295,9 +309,10 @@ module Flapjack
|
|
295
309
|
def last_notification
|
296
310
|
nils = { :type => nil, :timestamp => nil }
|
297
311
|
lne = last_notifications_of_each_type
|
298
|
-
ln = lne.delete_if {|type, timestamp|
|
299
|
-
|
300
|
-
|
312
|
+
ln = lne.delete_if {|type, timestamp| timestamp.nil? || timestamp.to_i == 0 }
|
313
|
+
if ln.find {|type, timestamp| type == :warning or type == :critical}
|
314
|
+
ln = ln.delete_if {|type, timestamp| type == :problem }
|
315
|
+
end
|
301
316
|
return nils unless ln.length > 0
|
302
317
|
lns = ln.sort_by { |type, timestamp| timestamp }.last
|
303
318
|
{ :type => lns[0], :timestamp => lns[1] }
|
@@ -422,11 +437,11 @@ module Flapjack
|
|
422
437
|
|
423
438
|
private
|
424
439
|
|
425
|
-
def initialize(
|
440
|
+
def initialize(entity, check, options = {})
|
426
441
|
raise "Redis connection not set" unless @redis = options[:redis]
|
427
|
-
raise "Invalid entity" unless @entity =
|
428
|
-
raise "Invalid check" unless @check =
|
429
|
-
@key = "#{
|
442
|
+
raise "Invalid entity" unless @entity = entity
|
443
|
+
raise "Invalid check" unless @check = check
|
444
|
+
@key = "#{entity.name}:#{check}"
|
430
445
|
end
|
431
446
|
|
432
447
|
def validate_state(state)
|