flapjack 0.7.27 → 0.7.28

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 (64) hide show
  1. data/.gitignore +1 -0
  2. data/CHANGELOG.md +9 -0
  3. data/bin/flapjack +22 -28
  4. data/bin/flapjack-nagios-receiver +5 -27
  5. data/bin/flapjack-populator +2 -2
  6. data/bin/flapper +13 -14
  7. data/bin/receive-events +3 -20
  8. data/bin/simulate-failed-check +3 -20
  9. data/etc/flapjack_config.yaml.example +119 -86
  10. data/features/cli.feature +69 -0
  11. data/features/events.feature +15 -0
  12. data/features/packaging-lintian.feature +4 -6
  13. data/features/rollup.feature +198 -0
  14. data/features/steps/cli_steps.rb +81 -0
  15. data/features/steps/events_steps.rb +26 -16
  16. data/features/steps/notifications_steps.rb +2 -2
  17. data/features/steps/packaging-lintian_steps.rb +2 -2
  18. data/features/support/daemons.rb +113 -0
  19. data/features/support/env.rb +26 -4
  20. data/lib/flapjack/configuration.rb +2 -0
  21. data/lib/flapjack/data/contact.rb +76 -5
  22. data/lib/flapjack/data/entity_check.rb +16 -0
  23. data/lib/flapjack/data/message.rb +11 -8
  24. data/lib/flapjack/data/notification.rb +31 -3
  25. data/lib/flapjack/data/notification_rule.rb +1 -1
  26. data/lib/flapjack/filters/delays.rb +1 -5
  27. data/lib/flapjack/gateways/api/contact_methods.rb +12 -6
  28. data/lib/flapjack/gateways/email.rb +35 -26
  29. data/lib/flapjack/gateways/email/alert.html.erb +4 -4
  30. data/lib/flapjack/gateways/email/alert.text.erb +2 -2
  31. data/lib/flapjack/gateways/email/alert_subject.text.erb +14 -0
  32. data/lib/flapjack/gateways/email/rollup.html.erb +48 -0
  33. data/lib/flapjack/gateways/email/rollup.text.erb +20 -0
  34. data/lib/flapjack/gateways/email/rollup_subject.text.erb +19 -0
  35. data/lib/flapjack/gateways/jabber.rb +97 -47
  36. data/lib/flapjack/gateways/sms_messagenet.rb +26 -24
  37. data/lib/flapjack/gateways/sms_messagenet/alert.text.erb +15 -0
  38. data/lib/flapjack/gateways/sms_messagenet/rollup.text.erb +34 -0
  39. data/lib/flapjack/gateways/web/views/contact.html.erb +16 -8
  40. data/lib/flapjack/notifier.rb +17 -4
  41. data/lib/flapjack/processor.rb +1 -1
  42. data/lib/flapjack/version.rb +1 -1
  43. data/spec/lib/flapjack/coordinator_spec.rb +19 -19
  44. data/spec/lib/flapjack/data/contact_spec.rb +100 -25
  45. data/spec/lib/flapjack/data/event_spec.rb +1 -1
  46. data/spec/lib/flapjack/data/message_spec.rb +1 -1
  47. data/spec/lib/flapjack/data/notification_spec.rb +11 -3
  48. data/spec/lib/flapjack/gateways/api/contact_methods_spec.rb +36 -17
  49. data/spec/lib/flapjack/gateways/api/entity_check_presenter_spec.rb +1 -1
  50. data/spec/lib/flapjack/gateways/api/entity_methods_spec.rb +38 -38
  51. data/spec/lib/flapjack/gateways/api/entity_presenter_spec.rb +15 -15
  52. data/spec/lib/flapjack/gateways/email_spec.rb +4 -4
  53. data/spec/lib/flapjack/gateways/jabber_spec.rb +13 -14
  54. data/spec/lib/flapjack/gateways/oobetet_spec.rb +2 -2
  55. data/spec/lib/flapjack/gateways/pagerduty_spec.rb +5 -5
  56. data/spec/lib/flapjack/gateways/sms_messagenet.spec.rb +1 -1
  57. data/spec/lib/flapjack/gateways/web/views/contact.html.erb_spec.rb +2 -2
  58. data/spec/lib/flapjack/gateways/web_spec.rb +4 -4
  59. data/spec/lib/flapjack/logger_spec.rb +3 -3
  60. data/spec/lib/flapjack/pikelet_spec.rb +10 -10
  61. data/spec/lib/flapjack/processor_spec.rb +4 -4
  62. data/spec/lib/flapjack/redis_pool_spec.rb +1 -1
  63. metadata +70 -5
  64. checksums.yaml +0 -15
@@ -136,7 +136,7 @@ end
136
136
 
137
137
  # TODO may need to get more complex, depending which SMS provider is used
138
138
  When /^the SMS notification handler runs successfully$/ do
139
- @request = stub_request(:get, /^#{Regexp.escape(Flapjack::Gateways::SmsMessagenet::MESSAGENET_URL)}/)
139
+ @request = stub_request(:get, /^#{Regexp.escape(Flapjack::Gateways::SmsMessagenet::MESSAGENET_DEFAULT_URL)}/)
140
140
 
141
141
  Flapjack::Gateways::SmsMessagenet.instance_variable_set('@config', {'username' => 'abcd', 'password' => 'efgh'})
142
142
  Flapjack::Gateways::SmsMessagenet.instance_variable_set('@redis', @redis)
@@ -147,7 +147,7 @@ When /^the SMS notification handler runs successfully$/ do
147
147
  end
148
148
 
149
149
  When /^the SMS notification handler fails to send an SMS$/ do
150
- @request = stub_request(:get, /^#{Regexp.escape(Flapjack::Gateways::SmsMessagenet::MESSAGENET_URL)}/).to_return(:status => [500, "Internal Server Error"])
150
+ @request = stub_request(:get, /^#{Regexp.escape(Flapjack::Gateways::SmsMessagenet::MESSAGENET_DEFAULT_URL)}/).to_return(:status => [500, "Internal Server Error"])
151
151
  Flapjack::Gateways::SmsMessagenet.instance_variable_set('@config', {'username' => 'abcd', 'password' => 'efgh'})
152
152
  Flapjack::Gateways::SmsMessagenet.instance_variable_set('@redis', @redis)
153
153
  Flapjack::Gateways::SmsMessagenet.instance_variable_set('@logger', @logger)
@@ -12,7 +12,7 @@ Then /^every file in the output should start with "([^\"]*)"$/ do |string|
12
12
  end
13
13
  end
14
14
 
15
- When /^I run "([^"]*)"$/ do |cmd|
15
+ When /^I run `([^"]*)`$/ do |cmd|
16
16
  #bin_path = '/usr/bin'
17
17
  #command = "#{bin_path}/#{cmd}"
18
18
 
@@ -21,7 +21,7 @@ When /^I run "([^"]*)"$/ do |cmd|
21
21
  @exit_status = $?.exitstatus
22
22
  end
23
23
 
24
- Then /^the exit status should be (\d+)$/ do |number|
24
+ Then /^the exit value should be (\d+)$/ do |number|
25
25
  @exit_status.should == number.to_i
26
26
  end
27
27
 
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ def red(text); "\e[0;31;49m#{text}\e[0m"; end
4
+ def yellow(text); "\e[0;33;49m#{text}\e[0m"; end
5
+ def green(text); "\e[0;32;49m#{text}\e[0m"; end
6
+ def blue(text); "\e[0;34;49m#{text}\e[0m"; end
7
+
8
+ After('@process') do
9
+ kill_lingering_processes
10
+ end
11
+
12
+ PROCESSES = []
13
+
14
+ def current_dir
15
+ File.join(*dirs)
16
+ end
17
+
18
+ def dirs
19
+ @dirs ||= ['tmp', 'cucumber_cli']
20
+ end
21
+
22
+ def write_file(file_name, file_content)
23
+ _create_file(file_name, file_content, false)
24
+ end
25
+
26
+ def _create_file(file_name, file_content, check_presence)
27
+ in_current_dir do
28
+ raise "expected #{file_name} to be present" if check_presence && !File.file?(file_name)
29
+ _mkdir(File.dirname(file_name))
30
+ File.open(file_name, 'w') { |f| f << file_content }
31
+ end
32
+ end
33
+
34
+ def in_current_dir(&block)
35
+ _mkdir(current_dir)
36
+ Dir.chdir(current_dir, &block)
37
+ end
38
+
39
+ def _mkdir(dir_name)
40
+ FileUtils.mkdir_p(dir_name) unless File.directory?(dir_name)
41
+ end
42
+
43
+ def kill_lingering_processes
44
+ PROCESSES && PROCESSES.each do |process_h|
45
+ process = process_h[:process]
46
+ pid = process ? process.pid : process_h[:pid]
47
+ command = process_h[:command]
48
+
49
+ begin
50
+ puts yellow("Killing process #{pid}") if @debug
51
+ Process.kill("KILL", pid)
52
+ rescue Errno::ESRCH
53
+ puts green("Process #{pid} has already exited.") if @debug
54
+ end
55
+
56
+ if @debug
57
+ if process
58
+ puts blue("Output from #{pid} #{command}\n")
59
+ puts process.read + "\n"
60
+ else
61
+ # TODO capture STDOUT from daemonize?
62
+ end
63
+ end
64
+ end
65
+ puts yellow("Done killing processes") if @debug
66
+ PROCESSES.clear
67
+ end
68
+
69
+ def time_and_pid_from_file(pid_file, cutoff_time = nil)
70
+ return unless File.exists?(pid_file)
71
+ file_time = File.mtime(pid_file)
72
+ return unless cutoff_time.nil? || (file_time.to_i >= cutoff_time.to_i)
73
+ [file_time, File.read(pid_file).strip.to_i]
74
+ end
75
+
76
+ def spawn_process(command, opts={})
77
+ puts yellow("Running: #{command}") if @debug
78
+
79
+ process_h = nil
80
+
81
+ file = opts[:daemon_pidfile]
82
+
83
+ if file
84
+ time = Time.now
85
+ attempts = 0
86
+
87
+ `#{command}`
88
+
89
+ while attempts < 50
90
+ time_and_pid = time_and_pid_from_file(file, time)
91
+ if time_and_pid
92
+ process_h = {:pid => time_and_pid.last, :command => command}
93
+ break
94
+ end
95
+ attempts += 1
96
+ sleep 0.2
97
+ end
98
+ else
99
+ process = IO.popen(command)
100
+ pid = process.pid
101
+ process_h = {:process => process, :command => command}
102
+ end
103
+
104
+ raise "failed to spawn process #{command}" if process_h.nil?
105
+
106
+ PROCESSES << process_h
107
+
108
+ process_h
109
+ end
110
+
111
+ at_exit do
112
+ kill_lingering_processes
113
+ end
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
2
  #
3
+
3
4
  require 'delorean'
4
5
  require 'chronic'
5
6
  require 'active_support/time'
@@ -12,6 +13,10 @@ if ENV['COVERAGE']
12
13
  SimpleCov.start do
13
14
  add_filter '/features/'
14
15
  end
16
+ SimpleCov.at_exit do
17
+ Oj.default_options = { :mode => :compat }
18
+ SimpleCov.result.format!
19
+ end
15
20
  end
16
21
 
17
22
  ENV["FLAPJACK_ENV"] = 'test'
@@ -24,6 +29,11 @@ require 'pathname'
24
29
  require 'webmock/cucumber'
25
30
  WebMock.disable_net_connect!
26
31
 
32
+ require 'oj'
33
+ Oj.mimic_JSON
34
+ Oj.default_options = { :indent => 0, :mode => :strict }
35
+ require 'active_support/json'
36
+
27
37
  require 'flapjack/notifier'
28
38
  require 'flapjack/processor'
29
39
  require 'flapjack/patches'
@@ -81,11 +91,11 @@ EXPIRE_AS_IF_AT
81
91
  end
82
92
 
83
93
  def self.time_travel_to(dest_time)
84
- # puts "travelling to #{Time.now.in_time_zone}, real time is #{Time.now_without_delorean.in_time_zone}"
94
+ #puts "travelling to #{Time.now.in_time_zone}, real time is #{Time.now_without_delorean.in_time_zone}"
85
95
  old_maybe_fake_time = Time.now.in_time_zone
86
96
 
87
97
  Delorean.time_travel_to(dest_time)
88
- # puts "travelled to #{Time.now.in_time_zone}, real time is #{Time.now_without_delorean.in_time_zone}"
98
+ #puts "travelled to #{Time.now.in_time_zone}, real time is #{Time.now_without_delorean.in_time_zone}"
89
99
  return if dest_time < old_maybe_fake_time
90
100
 
91
101
  # dumps the first offset -- we're not interested in time difference from
@@ -98,11 +108,11 @@ EXPIRE_AS_IF_AT
98
108
  delta = -offsets.inject(0){ |sum, val| sum + val }.floor
99
109
 
100
110
  real_time = Time.now_without_delorean.to_i
101
- # puts "delta #{delta}, expire before real time #{Time.at(real_time + delta)}"
111
+ #puts "delta #{delta}, expire before real time #{Time.at(real_time + delta)}"
102
112
 
103
113
  result = @redis.evalsha(@expire_as_if_at_sha, ['*'],
104
114
  [real_time, real_time + delta])
105
- # puts "Expired #{result} key#{(result == 1) ? '' : 's'}"
115
+ #puts "Expired #{result} key#{(result == 1) ? '' : 's'}"
106
116
  end
107
117
 
108
118
  end
@@ -170,3 +180,15 @@ After('@time') do
170
180
  Delorean.back_to_the_present
171
181
  end
172
182
 
183
+ After('@process') do
184
+ ['tmp/cucumber_cli/flapjack_cfg.yml',
185
+ 'tmp/cucumber_cli/flapjack_cfg.yml.bak',
186
+ 'tmp/cucumber_cli/flapjack_cfg_d.yml',
187
+ 'tmp/cucumber_cli/flapjack_d.log',
188
+ 'tmp/cucumber_cli/flapjack_d.pid'].each do |file|
189
+ next unless File.exists?(file)
190
+ File.unlink(file)
191
+ end
192
+ end
193
+
194
+
@@ -7,6 +7,8 @@ module Flapjack
7
7
 
8
8
  class Configuration
9
9
 
10
+ DEFAULT_CONFIG_PATH = '/etc/flapjack/flapjack_config.yaml'
11
+
10
12
  attr_reader :filename
11
13
 
12
14
  def initialize(opts = {})
@@ -16,9 +16,10 @@ module Flapjack
16
16
 
17
17
  class Contact
18
18
 
19
- attr_accessor :id, :first_name, :last_name, :email, :media, :media_intervals, :pagerduty_credentials
19
+ attr_accessor :id, :first_name, :last_name, :email, :media, :media_intervals, :media_rollup_thresholds, :pagerduty_credentials
20
20
 
21
21
  TAG_PREFIX = 'contact_tag'
22
+ ALL_MEDIA = ['email', 'sms', 'jabber', 'pagerduty']
22
23
 
23
24
  def self.all(options = {})
24
25
  raise "Redis connection not set" unless redis = options[:redis]
@@ -40,7 +41,7 @@ module Flapjack
40
41
  # sanity check
41
42
  return unless redis.hexists("contact:#{contact_id}", 'first_name')
42
43
 
43
- contact = self.new(:id => contact_id, :redis => redis)
44
+ contact = self.new(:id => contact_id, :redis => redis, :logger => logger)
44
45
  contact.refresh
45
46
  contact
46
47
  end
@@ -77,6 +78,7 @@ module Flapjack
77
78
  @redis.hmget("contact:#{@id}", 'first_name', 'last_name', 'email')
78
79
  self.media = @redis.hgetall("contact_media:#{@id}")
79
80
  self.media_intervals = @redis.hgetall("contact_media_intervals:#{self.id}")
81
+ self.media_rollup_thresholds = @redis.hgetall("contact_media_rollup_thresholds:#{self.id}")
80
82
 
81
83
  # similar to code in instance method pagerduty_credentials
82
84
  if service_key = @redis.hget("contact_media:#{@id}", 'pagerduty')
@@ -117,6 +119,7 @@ module Flapjack
117
119
 
118
120
  @redis.del("contact:#{self.id}", "contact_media:#{self.id}",
119
121
  "contact_media_intervals:#{self.id}",
122
+ "contact_media_rollup_thresholds:#{self.id}",
120
123
  "contact_tz:#{self.id}", "contact_pagerduty:#{self.id}")
121
124
  end
122
125
 
@@ -177,8 +180,8 @@ module Flapjack
177
180
  :entities => [],
178
181
  :tags => Flapjack::Data::TagSet.new([]),
179
182
  :time_restrictions => [],
180
- :warning_media => ['email', 'sms', 'jabber', 'pagerduty'],
181
- :critical_media => ['email', 'sms', 'jabber', 'pagerduty'],
183
+ :warning_media => ALL_MEDIA,
184
+ :critical_media => ALL_MEDIA,
182
185
  :warning_blackhole => false,
183
186
  :critical_blackhole => false,
184
187
  }, :logger => opts[:logger])
@@ -216,6 +219,20 @@ module Flapjack
216
219
  self.media_intervals = @redis.hgetall("contact_media_intervals:#{self.id}")
217
220
  end
218
221
 
222
+ def rollup_threshold_for_media(media)
223
+ threshold = @redis.hget("contact_media_rollup_thresholds:#{self.id}", media)
224
+ (threshold.nil? || (threshold.to_i <= 0 )) ? nil : threshold.to_i
225
+ end
226
+
227
+ def set_rollup_threshold_for_media(media, threshold)
228
+ if threshold.nil?
229
+ @redis.hdel("contact_media_rollup_thresholds:#{self.id}", media)
230
+ return
231
+ end
232
+ @redis.hset("contact_media_rollup_thresholds:#{self.id}", media, threshold)
233
+ self.media_rollup_thresholds = @redis.hgetall("contact_media_rollup_thresholds:#{self.id}")
234
+ end
235
+
219
236
  def set_address_for_media(media, address)
220
237
  @redis.hset("contact_media:#{self.id}", media, address)
221
238
  if media == 'pagerduty'
@@ -229,6 +246,7 @@ module Flapjack
229
246
  def remove_media(media)
230
247
  @redis.hdel("contact_media:#{self.id}", media)
231
248
  @redis.hdel("contact_media_intervals:#{self.id}", media)
249
+ @redis.hdel("contact_media_rollup_thresholds:#{self.id}", media)
232
250
  if media == 'pagerduty'
233
251
  @redis.del("contact_pagerduty:#{self.id}")
234
252
  end
@@ -260,7 +278,57 @@ module Flapjack
260
278
  else
261
279
  @redis.set(key, 'd')
262
280
  @redis.expire(key, self.interval_for_media(media))
281
+ # TODO: #182 - update the alert history keys
282
+ end
283
+ end
284
+
285
+ def drop_rollup_notifications_for_media?(media)
286
+ @redis.exists("drop_rollup_alerts_for_contact:#{self.id}:#{media}")
287
+ end
288
+
289
+ def update_sent_rollup_alert_keys_for_media(media, opts = {})
290
+ delete = !! opts[:delete]
291
+ key = "drop_rollup_alerts_for_contact:#{self.id}:#{media}"
292
+ if delete
293
+ @redis.del(key)
294
+ else
295
+ @redis.set(key, 'd')
296
+ @redis.expire(key, self.interval_for_media(media))
297
+ end
298
+ end
299
+
300
+ def add_alerting_check_for_media(media, check)
301
+ @redis.zadd("contact_alerting_checks:#{self.id}:media:#{media}", Time.now.to_i, check)
302
+ end
303
+
304
+ def remove_alerting_check_for_media(media, check)
305
+ @redis.zrem("contact_alerting_checks:#{self.id}:media:#{media}", check)
306
+ end
307
+
308
+ # removes any checks that are in ok, scheduled or unscheduled maintenance
309
+ # from the alerting checks set for the given media
310
+ # returns the number of checks removed
311
+ def clean_alerting_checks_for_media(media)
312
+ key = "contact_alerting_checks:#{self.id}:media:#{media}"
313
+ cleaned = 0
314
+ alerting_checks_for_media(media).each do |check|
315
+ next unless Flapjack::Data::EntityCheck.state_for_event_id?(check, :redis => @redis) == 'ok' ||
316
+ Flapjack::Data::EntityCheck.in_unscheduled_maintenance_for_event_id?(check, :redis => @redis) ||
317
+ Flapjack::Data::EntityCheck.in_scheduled_maintenance_for_event_id?(check, :redis => @redis)
318
+
319
+ @logger.debug("removing from alerting checks for #{self.id}/#{media}: #{check}") if @logger
320
+ remove_alerting_check_for_media(media, check)
321
+ cleaned += 1
263
322
  end
323
+ cleaned
324
+ end
325
+
326
+ def alerting_checks_for_media(media)
327
+ @redis.zrange("contact_alerting_checks:#{self.id}:media:#{media}", 0, -1)
328
+ end
329
+
330
+ def count_alerting_checks_for_media(media)
331
+ @redis.zcard("contact_alerting_checks:#{self.id}:media:#{media}")
264
332
  end
265
333
 
266
334
  # FIXME
@@ -344,7 +412,8 @@ module Flapjack
344
412
 
345
413
  def initialize(options = {})
346
414
  raise "Redis connection not set" unless @redis = options[:redis]
347
- @id = options[:id]
415
+ @id = options[:id]
416
+ @logger = options[:logger]
348
417
  end
349
418
 
350
419
  # NB: should probably be called in the context of a Redis multi block; not doing so
@@ -360,6 +429,7 @@ module Flapjack
360
429
  unless contact_data['media'].nil?
361
430
  redis.del("contact_media:#{contact_id}")
362
431
  redis.del("contact_media_intervals:#{contact_id}")
432
+ redis.del("contact_media_rollup_thresholds:#{contact_id}")
363
433
  redis.del("contact_pagerduty:#{contact_id}")
364
434
 
365
435
  contact_data['media'].each_pair {|medium, details|
@@ -371,6 +441,7 @@ module Flapjack
371
441
  else
372
442
  redis.hset("contact_media:#{contact_id}", medium, details['address'])
373
443
  redis.hset("contact_media_intervals:#{contact_id}", medium, details['interval']) if details['interval']
444
+ redis.hset("contact_media_rollup_thresholds:#{contact_id}", medium, details['rollup_threshold']) if details['rollup_threshold']
374
445
  end
375
446
  }
376
447
  end
@@ -114,6 +114,21 @@ module Flapjack
114
114
  result
115
115
  end
116
116
 
117
+ def self.in_unscheduled_maintenance_for_event_id?(event_id, options)
118
+ raise "Redis connection not set" unless redis = options[:redis]
119
+ redis.exists("#{event_id}:unscheduled_maintenance")
120
+ end
121
+
122
+ def self.in_scheduled_maintenance_for_event_id?(event_id, options)
123
+ raise "Redis connection not set" unless redis = options[:redis]
124
+ redis.exists("#{event_id}:scheduled_maintenance")
125
+ end
126
+
127
+ def self.state_for_event_id?(event_id, options)
128
+ raise "Redis connection not set" unless redis = options[:redis]
129
+ redis.hget("check:#{event_id}", 'state')
130
+ end
131
+
117
132
  def entity_name
118
133
  entity.name
119
134
  end
@@ -536,6 +551,7 @@ module Flapjack
536
551
  raise "Invalid entity" unless @entity = entity
537
552
  raise "Invalid check" unless @check = check
538
553
  @key = "#{entity.name}:#{check}"
554
+ @logger = options[:logger]
539
555
  end
540
556
 
541
557
  end
@@ -10,13 +10,14 @@ module Flapjack
10
10
  module Data
11
11
  class Message
12
12
 
13
- attr_reader :medium, :address, :duration, :contact
13
+ attr_reader :medium, :address, :duration, :contact, :rollup
14
14
 
15
15
  def self.for_contact(contact, opts = {})
16
- self.new(:contact => contact,
17
- :medium => opts[:medium],
18
- :address => opts[:address],
19
- :duration => opts[:duration])
16
+ self.new(:contact => contact,
17
+ :medium => opts[:medium],
18
+ :address => opts[:address],
19
+ :duration => opts[:duration],
20
+ :rollup => opts[:rollup])
20
21
  end
21
22
 
22
23
  def id
@@ -31,6 +32,7 @@ module Flapjack
31
32
  c = {'media' => medium,
32
33
  'address' => address,
33
34
  'id' => id,
35
+ 'rollup' => rollup,
34
36
  'contact_id' => contact.id,
35
37
  'contact_first_name' => contact.first_name,
36
38
  'contact_last_name' => contact.last_name}
@@ -41,10 +43,11 @@ module Flapjack
41
43
  private
42
44
 
43
45
  def initialize(opts = {})
44
- @contact = opts[:contact]
45
- @medium = opts[:medium]
46
- @address = opts[:address]
46
+ @contact = opts[:contact]
47
+ @medium = opts[:medium]
48
+ @address = opts[:address]
47
49
  @duration = opts[:duration]
50
+ @rollup = opts[:rollup]
48
51
  end
49
52
 
50
53
  end