flapjack 0.8.10 → 0.8.11

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 (55) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +9 -0
  3. data/Gemfile +1 -1
  4. data/bin/flapjack +10 -1
  5. data/bin/flapjack-nagios-receiver +1 -2
  6. data/bin/simulate-failed-check +12 -4
  7. data/etc/flapjack_config.yaml.example +2 -1
  8. data/flapjack.gemspec +1 -0
  9. data/lib/flapjack/data/contact.rb +46 -26
  10. data/lib/flapjack/data/entity.rb +28 -0
  11. data/lib/flapjack/data/entity_check.rb +52 -11
  12. data/lib/flapjack/data/event.rb +9 -3
  13. data/lib/flapjack/data/notification_rule.rb +8 -0
  14. data/lib/flapjack/gateways/api.rb +0 -1
  15. data/lib/flapjack/gateways/api/entity_check_presenter.rb +2 -1
  16. data/lib/flapjack/gateways/email.rb +1 -2
  17. data/lib/flapjack/gateways/jabber.rb +3 -3
  18. data/lib/flapjack/gateways/jsonapi.rb +186 -38
  19. data/lib/flapjack/gateways/jsonapi/check_methods.rb +120 -0
  20. data/lib/flapjack/gateways/jsonapi/{entity_check_presenter.rb → check_presenter.rb} +7 -6
  21. data/lib/flapjack/gateways/jsonapi/contact_methods.rb +61 -352
  22. data/lib/flapjack/gateways/jsonapi/entity_methods.rb +117 -248
  23. data/lib/flapjack/gateways/jsonapi/medium_methods.rb +179 -0
  24. data/lib/flapjack/gateways/jsonapi/notification_rule_methods.rb +124 -0
  25. data/lib/flapjack/gateways/jsonapi/pagerduty_credential_methods.rb +128 -0
  26. data/lib/flapjack/gateways/jsonapi/rack/json_params_parser.rb +4 -5
  27. data/lib/flapjack/gateways/jsonapi/report_methods.rb +143 -0
  28. data/lib/flapjack/gateways/web.rb +1 -0
  29. data/lib/flapjack/gateways/web/public/js/backbone.jsonapi.js +165 -101
  30. data/lib/flapjack/gateways/web/public/js/contacts.js +34 -46
  31. data/lib/flapjack/gateways/web/public/js/select2.js +232 -90
  32. data/lib/flapjack/gateways/web/public/js/select2.min.js +4 -4
  33. data/lib/flapjack/gateways/web/views/check.html.erb +11 -2
  34. data/lib/flapjack/processor.rb +6 -6
  35. data/lib/flapjack/version.rb +1 -1
  36. data/spec/lib/flapjack/data/entity_check_spec.rb +1 -1
  37. data/spec/lib/flapjack/data/event_spec.rb +10 -9
  38. data/spec/lib/flapjack/gateways/api/entity_methods_spec.rb +25 -25
  39. data/spec/lib/flapjack/gateways/api_spec.rb +23 -1
  40. data/spec/lib/flapjack/gateways/email_spec.rb +40 -2
  41. data/spec/lib/flapjack/gateways/jabber_spec.rb +1 -1
  42. data/spec/lib/flapjack/gateways/jsonapi/check_methods_spec.rb +134 -0
  43. data/spec/lib/flapjack/gateways/jsonapi/{entity_check_presenter_spec.rb → check_presenter_spec.rb} +17 -17
  44. data/spec/lib/flapjack/gateways/jsonapi/contact_methods_spec.rb +27 -232
  45. data/spec/lib/flapjack/gateways/jsonapi/entity_methods_spec.rb +217 -687
  46. data/spec/lib/flapjack/gateways/jsonapi/medium_methods_spec.rb +232 -0
  47. data/spec/lib/flapjack/gateways/jsonapi/notification_rule_methods_spec.rb +131 -0
  48. data/spec/lib/flapjack/gateways/jsonapi/pagerduty_credential_methods_spec.rb +113 -0
  49. data/spec/lib/flapjack/gateways/jsonapi/report_methods_spec.rb +546 -0
  50. data/spec/lib/flapjack/gateways/jsonapi_spec.rb +10 -1
  51. data/spec/lib/flapjack/gateways/web_spec.rb +1 -0
  52. data/spec/support/jsonapi_helper.rb +62 -0
  53. metadata +36 -8
  54. data/lib/flapjack/gateways/jsonapi/entity_presenter.rb +0 -75
  55. data/spec/lib/flapjack/gateways/jsonapi/entity_presenter_spec.rb +0 -108
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: de195d7c1f069b7e1817c654b51315c55df5ec37
4
- data.tar.gz: b6f8b5059820e001df53e1a7f3ef6fdbd49c90cb
3
+ metadata.gz: 20501028ceb91b915d143e9d9343fae832db8e1c
4
+ data.tar.gz: b2d975287acd597e04f148f589c76ed067832665
5
5
  SHA512:
6
- metadata.gz: e0131f314a1c13869035a9ec07cbca94bd8a79b56af800b7183291e50fade89bccd023223ccf2b7037d0c8bab9fc20ca25aec348e0e0ca8baf371071f530ad47
7
- data.tar.gz: 9710ad57f6ced3f0b4f276690dce38e5accd4361613ef3944811893d7faa46c5871e92e07da2cd30428d7a85cfc2b3d7887067c9b1f068565fc577410c7538c6
6
+ metadata.gz: 051d4c2f9f58b9b57a45105a10c7cd64353c2a3c485b53a57a8bcc54f7546ed36058a50dfcb52a663413ffc2f8d5c955651a5cb554a48ea276b073eb93345409
7
+ data.tar.gz: ea18f80815f9172ca9a0f3f2021934b9f38d5da6cec030c0f27ad493dec8178ef40120f3004a898adfe50f6a1df22b0ac89c45a8a6492110694f02d2c5986e8f
data/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  ## Flapjack Changelog
2
2
 
3
+ # 0.8.11 - 2014-05-01
4
+ - Feature: simulate-failed-check - give -t a default of 45 seconds (@jessereynolds)
5
+ - Feature: allow email messages to have custom from address #468 (@mattdelves)
6
+ - Feature: jabber - The reply message should only include the filtered list of entity checks #472 (@someword)
7
+ - Feature: Add perfdata handling #471 (@mattdelves)
8
+ - Feature: jsonapi featureset reaches critical mass #474 (@ali-graham)
9
+ - Feature: Add rbtrace command line option for profiling #479 (@auxesis)
10
+ - Bug: ack'ing via jabber fails #480 (@jessereynolds)
11
+
3
12
  # 0.8.10 - 2014-04-28
4
13
  - Feature: Add regex entities to notification rules #463 (@jswoods)
5
14
  - Bug: oobetet gateway exception sending pagerduty event #464 (@jessereynolds)
data/Gemfile CHANGED
@@ -11,7 +11,7 @@ group :test do
11
11
  gem 'cucumber'
12
12
  gem 'delorean'
13
13
  gem 'rack-test'
14
- gem 'async_rack_test'
14
+ gem 'async_rack_test', '>= 0.0.5'
15
15
  gem 'resque_spec'
16
16
  gem 'webmock'
17
17
  gem 'guard'
data/bin/flapjack CHANGED
@@ -53,10 +53,18 @@ optparse = OptionParser.new do |opts|
53
53
  options.config = c
54
54
  end
55
55
 
56
+ opts.on("-n", "--environment [ENV]", String, "Environment to boot") do |e|
57
+ options.environment = e
58
+ end
59
+
56
60
  opts.on("-d", "--[no-]daemonize", "Daemonize?") do |d|
57
61
  options.daemonize = d
58
62
  end
59
63
 
64
+ opts.on('-r', '--rbtrace', 'Enable rbtrace profiling') do
65
+ require 'rbtrace'
66
+ end
67
+
60
68
  opts.on("-p", "--pidfile [PATH]", String, "PATH to the pidfile to write to") do |pid|
61
69
  options.pidfile = pid
62
70
  end
@@ -88,7 +96,7 @@ elsif !["start", "stop", "restart", "reload", "status"].include?(ARGV[0])
88
96
  exit 1
89
97
  end
90
98
 
91
- FLAPJACK_ENV = ENV['FLAPJACK_ENV'] || 'production'
99
+ FLAPJACK_ENV = options.environment || ENV['FLAPJACK_ENV'] || 'production'
92
100
 
93
101
  config = Flapjack::Configuration.new
94
102
  config.load(options.config)
@@ -158,6 +166,7 @@ when "start"
158
166
  puts "Flapjack is already running."
159
167
  else
160
168
  print "Flapjack starting..."
169
+ print "\n" unless daemonize
161
170
  return_value = nil
162
171
  runner.execute(:daemonize => daemonize) {
163
172
  return_value = flapjack_coord.call
@@ -59,6 +59,7 @@ def process_input(opts)
59
59
  'state' => state,
60
60
  'summary' => check_output,
61
61
  'details' => details,
62
+ 'perfdata' => check_perfdata,
62
63
  'time' => timestamp,
63
64
  }
64
65
  Flapjack::Data::Event.add(event, :redis => redis)
@@ -243,5 +244,3 @@ else
243
244
  exit 1
244
245
 
245
246
  end
246
-
247
-
@@ -26,7 +26,7 @@ end
26
26
 
27
27
  def fail(opts)
28
28
  redis = Redis.new(opts[:redis_options])
29
- stop_after = opts[:minutes].to_i * 60
29
+ stop_after = (opts[:minutes] * 60).to_i
30
30
  recover = opts[:recover]
31
31
  state = opts[:state] || 'critical'
32
32
  event = {
@@ -82,8 +82,12 @@ optparse = OptionParser.new do |opts|
82
82
  options.config = c
83
83
  end
84
84
 
85
- opts.on("-t", "--time MINUTES", String, "MINUTES to generate failure events for") do |t|
86
- options.minutes = t
85
+ opts.on("-n", "--environment [ENV]", String, "Environment to boot") do |e|
86
+ options.environment = e
87
+ end
88
+
89
+ opts.on("-t", "--time MINUTES", String, "MINUTES to generate failure events for (0.75)") do |t|
90
+ options.minutes = t.to_f
87
91
  end
88
92
 
89
93
  opts.on("-i", "--interval SECONDS", String, "SECONDS between events, can be decimal eg 0.1 (10)") do |i|
@@ -109,7 +113,11 @@ unless options.interval.to_f > 0
109
113
  options.interval = 10.0
110
114
  end
111
115
 
112
- FLAPJACK_ENV = ENV['FLAPJACK_ENV'] || 'production'
116
+ unless options.minutes
117
+ options.minutes = 0.75
118
+ end
119
+
120
+ FLAPJACK_ENV = options.environment || ENV['FLAPJACK_ENV'] || 'production'
113
121
 
114
122
  config = Flapjack::Configuration.new
115
123
  config.load(options.config)
@@ -49,8 +49,9 @@ production:
49
49
  level: INFO
50
50
  syslog_errors: yes
51
51
  smtp_config:
52
- # port 1025 is the default port for http://mailcatcher.me
52
+ #from: "flapjack@noreply.example"
53
53
  host: 127.0.0.1
54
+ # 1025 is the default port for http://mailcatcher.me
54
55
  port: 1025
55
56
  starttls: false
56
57
  #auth:
data/flapjack.gemspec CHANGED
@@ -38,6 +38,7 @@ Gem::Specification.new do |gem|
38
38
  gem.add_dependency 'ice_cube'
39
39
  gem.add_dependency 'tzinfo', '~> 1.0.1'
40
40
  gem.add_dependency 'tzinfo-data'
41
+ gem.add_dependency 'rbtrace'
41
42
 
42
43
  gem.add_development_dependency 'rake'
43
44
  end
@@ -19,8 +19,7 @@ module Flapjack
19
19
  class Contact
20
20
 
21
21
  attr_accessor :id, :first_name, :last_name, :email, :media,
22
- :media_intervals, :media_rollup_thresholds, :pagerduty_credentials,
23
- :linked_entity_ids, :linked_media_ids
22
+ :media_intervals, :media_rollup_thresholds, :pagerduty_credentials
24
23
 
25
24
  TAG_PREFIX = 'contact_tag'
26
25
  ALL_MEDIA = ['email', 'sms', 'jabber', 'pagerduty']
@@ -155,6 +154,11 @@ module Flapjack
155
154
  *['subdomain', 'username', 'password'].collect {|f| [f, details[f]]})
156
155
  end
157
156
 
157
+ def delete_pagerduty_credentials
158
+ @redis.hdel("contact_media:#{self.id}", 'pagerduty')
159
+ @redis.del("contact_pagerduty:#{self.id}")
160
+ end
161
+
158
162
  # returns false if this contact was already in the set for the entity
159
163
  def add_entity(entity)
160
164
  key = "contacts_for:#{entity.id}"
@@ -201,11 +205,10 @@ module Flapjack
201
205
  }.values
202
206
  end
203
207
 
204
- def self.entities_jsonapi(contact_ids, options = {})
208
+ def self.entity_ids_for(contact_ids, options = {})
205
209
  raise "Redis connection not set" unless redis = options[:redis]
206
210
 
207
- entity_data = []
208
- linked_entity_ids = {}
211
+ entity_ids = {}
209
212
 
210
213
  temp_set = SecureRandom.uuid
211
214
  redis.sadd(temp_set, contact_ids)
@@ -216,28 +219,30 @@ module Flapjack
216
219
  next unless k =~ /^contacts_for:([a-zA-Z0-9][a-zA-Z0-9\.\-]*[a-zA-Z0-9])(?::(\w+))?$/
217
220
 
218
221
  entity_id = $1
219
- check = $2
220
-
221
- entity_data << {:id => entity_id, :name => redis.hget("entity:#{entity_id}", 'name')}
222
+ # check = $2
222
223
 
223
224
  contact_ids.each do |contact_id|
224
- linked_entity_ids[contact_id] ||= []
225
- linked_entity_ids[contact_id] << entity_id
225
+ entity_ids[contact_id] ||= []
226
+ entity_ids[contact_id] << entity_id
226
227
  end
227
228
  end
228
229
 
229
230
  redis.del(temp_set)
230
231
 
231
- [entity_data, linked_entity_ids]
232
+ entity_ids
232
233
  end
233
234
 
234
235
  def name
235
236
  [(self.first_name || ''), (self.last_name || '')].join(" ").strip
236
237
  end
237
238
 
239
+ def notification_rule_ids
240
+ @redis.smembers("contact_notification_rules:#{self.id}")
241
+ end
242
+
238
243
  # return an array of the notification rules of this contact
239
244
  def notification_rules(opts = {})
240
- rules = @redis.smembers("contact_notification_rules:#{self.id}").inject([]) do |ret, rule_id|
245
+ rules = self.notification_rule_ids.inject([]) do |ret, rule_id|
241
246
  unless (rule_id.nil? || rule_id == '')
242
247
  ret << Flapjack::Data::NotificationRule.find_by_id(rule_id, :redis => @redis)
243
248
  end
@@ -268,6 +273,14 @@ module Flapjack
268
273
  :redis => @redis, :logger => opts[:logger])
269
274
  end
270
275
 
276
+ # move an existing notification rule from another contact to this one
277
+ def grab_notification_rule(rule)
278
+ @redis.srem("contact_notification_rules:#{rule.contact.id}", rule.id)
279
+ rule.contact_id = self.id
280
+ rule.update({})
281
+ @redis.sadd("contact_notification_rules:#{self.id}", rule.id)
282
+ end
283
+
271
284
  def delete_notification_rule(rule)
272
285
  @redis.srem("contact_notification_rules:#{self.id}", rule.id)
273
286
  @redis.del("notification_rule:#{rule.id}")
@@ -436,7 +449,11 @@ module Flapjack
436
449
  # return a list of media enabled for this contact
437
450
  # eg [ 'email', 'sms' ]
438
451
  def media_list
439
- @redis.hkeys("contact_media:#{self.id}")
452
+ @redis.hkeys("contact_media:#{self.id}") - ['pagerduty']
453
+ end
454
+
455
+ def media_ids
456
+ self.media_list.collect {|medium| "#{self.id}_#{medium}" }
440
457
  end
441
458
 
442
459
  # return the timezone of the contact, or the system default if none is set
@@ -484,16 +501,17 @@ module Flapjack
484
501
  }.to_json
485
502
  end
486
503
 
487
- def to_jsonapi(*args)
488
- { "id" => self.id,
489
- "first_name" => self.first_name,
490
- "last_name" => self.last_name,
491
- "email" => self.email,
492
- "timezone" => self.timezone.name,
493
- "tags" => self.tags.to_a,
494
- "links" => {
495
- :entities => @linked_entity_ids || [],
496
- :media => @linked_media_ids || []
504
+ def to_jsonapi(opts = {})
505
+ { "id" => self.id,
506
+ "first_name" => self.first_name,
507
+ "last_name" => self.last_name,
508
+ "email" => self.email,
509
+ "timezone" => self.timezone.name,
510
+ "tags" => self.tags.to_a,
511
+ "links" => {
512
+ :entities => opts[:entity_ids] || [],
513
+ :media => self.media_ids || [],
514
+ :notification_rules => self.notification_rule_ids || [],
497
515
  }
498
516
  }.to_json
499
517
  end
@@ -512,9 +530,11 @@ module Flapjack
512
530
  def self.add_or_update(contact_id, contact_data, options = {})
513
531
  raise "Redis connection not set" unless redis = options[:redis]
514
532
 
515
- # TODO check that the rest of this is safe for the update case
516
- redis.hmset("contact:#{contact_id}",
517
- *['first_name', 'last_name', 'email'].collect {|f| [f, contact_data[f]]})
533
+ attrs = (['first_name', 'last_name', 'email'] & contact_data.keys).collect do |key|
534
+ [key, contact_data[key]]
535
+ end.flatten(1)
536
+
537
+ redis.hmset("contact:#{contact_id}", *attrs) unless attrs.empty?
518
538
 
519
539
  if ( ! contact_data['tags'].nil? && contact_data['tags'].is_a?(Enumerable))
520
540
  contact_data['tags'].each do |t|
@@ -78,6 +78,15 @@ module Flapjack
78
78
  self.new(:name => entity_name, :id => entity_id, :redis => redis)
79
79
  end
80
80
 
81
+ def self.find_by_ids(entity_ids, options = {})
82
+ raise "Redis connection not set" unless redis = options[:redis]
83
+ logger = options[:logger]
84
+
85
+ entity_ids.map do |id|
86
+ self.find_by_id(id, options)
87
+ end
88
+ end
89
+
81
90
  # NB: if we're worried about user input, https://github.com/mudge/re2
82
91
  # has bindings for a non-backtracking RE engine that runs in linear
83
92
  # time
@@ -145,6 +154,15 @@ module Flapjack
145
154
  }.compact
146
155
  end
147
156
 
157
+ def self.contact_ids_for(entity_ids, options = {})
158
+ raise "Redis connection not set" unless redis = options[:redis]
159
+
160
+ entity_ids.inject({}) do |memo, entity_id|
161
+ memo[entity_id] = redis.smembers("contacts_for:#{entity_id}")
162
+ memo
163
+ end
164
+ end
165
+
148
166
  def check_list
149
167
  @redis.zrange("current_checks:#{@name}", 0, -1)
150
168
  end
@@ -186,6 +204,16 @@ module Flapjack
186
204
  }
187
205
  end
188
206
 
207
+ def to_jsonapi(opts = {})
208
+ {
209
+ "id" => self.id,
210
+ "name" => self.name,
211
+ "links" => {
212
+ :contacts => opts[:contact_ids] || [],
213
+ }
214
+ }.to_json
215
+ end
216
+
189
217
  private
190
218
 
191
219
  # NB: initializer should not be used directly -- instead one of the finder methods
@@ -27,31 +27,38 @@ module Flapjack
27
27
 
28
28
  attr_accessor :entity, :check
29
29
 
30
- # TODO probably shouldn't always be creating on query -- work out when this should be happening
31
30
  def self.for_event_id(event_id, options = {})
32
31
  raise "Redis connection not set" unless redis = options[:redis]
32
+ entity_name, check_name = event_id.split(':', 2)
33
+ create_entity = options[:create_entity]
33
34
  logger = options[:logger]
34
- entity_name, check = event_id.split(':', 2)
35
- self.new(Flapjack::Data::Entity.find_by_name(entity_name, :redis => redis, :create => true, :logger => true), check,
36
- :redis => redis, :logger => logger)
35
+ entity = Flapjack::Data::Entity.find_by_name(entity_name,
36
+ :create => create_entity, :logger => logger, :redis => redis)
37
+ self.new(entity, check_name, :logger => logger, :redis => redis)
37
38
  end
38
39
 
39
- # TODO probably shouldn't always be creating on query -- work out when this should be happening
40
- def self.for_entity_name(entity_name, check, options = {})
40
+ def self.for_entity_name(entity_name, check_name, options = {})
41
41
  raise "Redis connection not set" unless redis = options[:redis]
42
- self.new(Flapjack::Data::Entity.find_by_name(entity_name, :redis => redis, :create => true), check,
43
- :redis => redis)
42
+ create_entity = options[:create_entity]
43
+ logger = options[:logger]
44
+ entity = Flapjack::Data::Entity.find_by_name(entity_name,
45
+ :create => create_entity, :logger => logger, :redis => redis)
46
+ self.new(entity, check_name, :logger => logger, :redis => redis)
44
47
  end
45
48
 
46
49
  def self.for_entity_id(entity_id, check, options = {})
47
50
  raise "Redis connection not set" unless redis = options[:redis]
48
- self.new(Flapjack::Data::Entity.find_by_id(entity_id, :redis => redis), check,
49
- :redis => redis)
51
+ create_entity = options[:create_entity]
52
+ logger = options[:logger]
53
+ entity = Flapjack::Data::Entity.find_by_id(entity_id,
54
+ :create => create_entity, :logger => logger, :redis => redis)
55
+ self.new(entity, check, :redis => redis)
50
56
  end
51
57
 
52
58
  def self.for_entity(entity, check, options = {})
53
59
  raise "Redis connection not set" unless redis = options[:redis]
54
- self.new(entity, check, :redis => redis)
60
+ logger = options[:logger]
61
+ self.new(entity, check, :logger => logger, :redis => redis)
55
62
  end
56
63
 
57
64
  def self.find_all_for_entity_name(entity_name, options = {})
@@ -360,6 +367,7 @@ module Flapjack
360
367
  timestamp = options[:timestamp] || Time.now.to_i
361
368
  summary = options[:summary]
362
369
  details = options[:details]
370
+ perfdata = options[:perfdata]
363
371
  count = options[:count]
364
372
 
365
373
  old_state = self.state
@@ -400,6 +408,10 @@ module Flapjack
400
408
  # hash summary and details (as they may have changed)
401
409
  @redis.hset("check:#{@key}", 'summary', (summary || ''))
402
410
  @redis.hset("check:#{@key}", 'details', (details || ''))
411
+ if perfdata
412
+ @redis.hset("check:#{@key}", 'perfdata', format_perfdata(perfdata).to_json)
413
+ # @redis.set("#{@key}:#{timestamp}:perfdata", perfdata)
414
+ end
403
415
 
404
416
  @redis.exec
405
417
  end
@@ -503,6 +515,18 @@ module Flapjack
503
515
  @redis.hget("check:#{@key}", 'details')
504
516
  end
505
517
 
518
+ def perfdata
519
+ data = @redis.hget("check:#{@key}", 'perfdata')
520
+ begin
521
+ data = JSON.parse(data) if data
522
+ rescue
523
+ data = "Unable to parse string: #{data}"
524
+ end
525
+
526
+ data = [data] if data.is_a?(Hash)
527
+ data
528
+ end
529
+
506
530
  # Returns a list of states for this entity check, sorted by timestamp.
507
531
  #
508
532
  # start_time and end_time should be passed as integer timestamps; these timestamps
@@ -647,6 +671,23 @@ module Flapjack
647
671
  @logger = options[:logger]
648
672
  end
649
673
 
674
+ def format_perfdata(perfdata)
675
+ # example perfdata: time=0.486630s;;;0.000000 size=909B;;;0
676
+ items = perfdata.split(' ')
677
+ # Do some fancy regex
678
+ data = []
679
+ items.each do |item|
680
+ components = item.split '='
681
+ key = components[0].to_s
682
+ value = ""
683
+ if components[1]
684
+ value = components[1].split(';')[0].to_s
685
+ end
686
+ data << {"key" => key, "value" => value}
687
+ end
688
+ data
689
+ end
690
+
650
691
  end
651
692
 
652
693
  end