sensu 0.23.3 → 0.24.0.beta

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 2a163224e80319ffc8f609c822c4243991a86585
4
- data.tar.gz: f5f24edd6a1f730516ba87a3911b8da6fc0c2a31
3
+ metadata.gz: cf29524e586ad648f83f891749f04ed9bb41d1f5
4
+ data.tar.gz: 651104b216bbc218bebfc51ec19e46563a32c093
5
5
  SHA512:
6
- metadata.gz: 99e3e414e8b34e1b271c846c484c28433eedbc9aad76830c11eed1df07595d3715704216475edf3307d87f1b30105271e74a51d45671027f23efdf6e9d0f2457
7
- data.tar.gz: b48247feeedbcc6cfd60e9fd719484ef2f08bd0bf7ad79e4533d08e634722f2d159ebc1558fae477f3c13ee238c145da9115068ebf1fff79d75d2ac25fba06b9
6
+ metadata.gz: ee318b81fcda3b1b714052000cad8c7fede926803f31f96dd52f33a461e42b4b840addce01b950139feca58f6583cc65ee9dd1e3e42dea257a94182ec0df2f50
7
+ data.tar.gz: cfeee1f970ae412bd090004b6f977026382f7806c2e520363c11731885fd3a8f218c2aa4c19214d4035fec22fd6b54de6c97d0f6f9c4eaaafe6c2e0cd99b29a2
data/CHANGELOG.md CHANGED
@@ -1,3 +1,61 @@
1
+ ## 0.24.0 - TBD
2
+
3
+ ### Important
4
+
5
+ Sensu check ["Aggregates 2.0"](https://github.com/sensu/sensu/issues/1218)
6
+ breaks the existing Sensu API aggregate endpoints.
7
+
8
+ Sensu API GET /health endpoint, failed health checks now respond with a
9
+ `412` (preconditions failed) instead of a `503`.
10
+
11
+ ### Features
12
+
13
+ Persistent Sensu event IDs, event occurrences for a client/check pair will
14
+ now have the same event ID until the event is resolved.
15
+
16
+ Added a CLI option/argument to cause the Sensu service to validate its
17
+ compiled configuration settings and exit with the appropriate exit
18
+ code, e.g. `2` for invalid. The CLI option is `--validate_config`. This
19
+ feature is now used when restarting a Sensu service to first validate the
20
+ new configuration before stopping the running service.
21
+
22
+ Improved tracking of in progress check result processing, no longer
23
+ potentially losing check results when restarting the Sensu server service.
24
+
25
+ Event data check type now explicitly defaults to "standard".
26
+
27
+ Check results for proxy clients (a.k.a JIT clients) will now have a check
28
+ "origin" set to the client name of the result producer.
29
+
30
+ Configurable Sensu Spawn concurrent child process limit (checks, mutators,
31
+ & pipe handlers). The default limit is still `12` and the EventMachine
32
+ threadpool size is automatically adjusted to accommodate a larger limit.
33
+
34
+ Sensu check ["Aggregates 2.0"](https://github.com/sensu/sensu/issues/1218).
35
+
36
+ Sensu client token substitution is now supported in every check definition
37
+ attribute value, no longer just the check command.
38
+
39
+ ### Other
40
+
41
+ Sensu API GET /health endpoint, failed health check now responds with a
42
+ `412` (preconditions failed) instead of a `503`.
43
+
44
+ Sensu API POST /clients endpoint can now create clients in the registry
45
+ that are expected to produce keepalives, and validates clients with the
46
+ Sensu Settings client definition validator.
47
+
48
+ Updated Thin (used by Sensu API) to the latest version, 1.6.4.
49
+
50
+ JrJackson is now used to parse JSON when Sensu is running on JRuby.
51
+
52
+ The Sensu API now listens immediately on service start, even before it has
53
+ successfully connected to Redis and the Sensu Transport. It will now
54
+ respond with a `500` response, with a descriptive error message, when it
55
+ has not yet initialized its connections or it is reconnecting to either
56
+ Redis or the Sensu Transport. The API /info and /health endpoints will
57
+ still respond normally while reconnecting.
58
+
1
59
  ## 0.23.3 - 2016-05-26
2
60
 
3
61
  ### Fixes
@@ -1,10 +1,11 @@
1
1
  require "sensu/daemon"
2
+ require "sensu/api/validators"
2
3
 
3
4
  gem "sinatra", "1.4.6"
4
5
  gem "async_sinatra", "1.2.0"
5
6
 
6
7
  unless RUBY_PLATFORM =~ /java/
7
- gem "thin", "1.6.3"
8
+ gem "thin", "1.6.4"
8
9
  require "thin"
9
10
  end
10
11
 
@@ -22,10 +23,9 @@ module Sensu
22
23
  bootstrap(options)
23
24
  setup_process(options)
24
25
  EM::run do
25
- setup_connections do
26
- start
27
- setup_signal_traps
28
- end
26
+ setup_connections
27
+ start
28
+ setup_signal_traps
29
29
  end
30
30
  end
31
31
 
@@ -84,8 +84,8 @@ module Sensu
84
84
  def stop
85
85
  @logger.warn("stopping")
86
86
  stop_server do
87
- settings.redis.close if settings.redis
88
- settings.transport.close if settings.transport
87
+ settings.redis.close if settings.respond_to?(:redis)
88
+ settings.transport.close if settings.respond_to?(:transport)
89
89
  super
90
90
  end
91
91
  end
@@ -124,11 +124,18 @@ module Sensu
124
124
  env["rack.input"].rewind
125
125
  end
126
126
 
127
- def protected!
128
- if settings.api[:user] && settings.api[:password]
129
- return if !(settings.api[:user] && settings.api[:password]) || authorized?
130
- headers["WWW-Authenticate"] = 'Basic realm="Restricted Area"'
131
- unauthorized!
127
+ def connected?
128
+ if settings.respond_to?(:redis) && settings.respond_to?(:transport)
129
+ unless ["/info", "/health"].include?(env["REQUEST_PATH"])
130
+ unless settings.redis.connected?
131
+ not_connected!("not connected to redis")
132
+ end
133
+ unless settings.transport.connected?
134
+ not_connected!("not connected to transport")
135
+ end
136
+ end
137
+ else
138
+ not_connected!("redis and transport connections not initialized")
132
139
  end
133
140
  end
134
141
 
@@ -140,6 +147,22 @@ module Sensu
140
147
  @auth.credentials == [settings.api[:user], settings.api[:password]]
141
148
  end
142
149
 
150
+ def protected!
151
+ if settings.api[:user] && settings.api[:password]
152
+ return if authorized?
153
+ headers["WWW-Authenticate"] = 'Basic realm="Restricted Area"'
154
+ unauthorized!
155
+ end
156
+ end
157
+
158
+ def error!(body="")
159
+ throw(:halt, [500, body])
160
+ end
161
+
162
+ def not_connected!(message)
163
+ error!(Sensu::JSON.dump(:error => message))
164
+ end
165
+
143
166
  def bad_request!
144
167
  ahalt 400
145
168
  end
@@ -152,8 +175,8 @@ module Sensu
152
175
  ahalt 404
153
176
  end
154
177
 
155
- def unavailable!
156
- ahalt 503
178
+ def precondition_failed!
179
+ ahalt 412
157
180
  end
158
181
 
159
182
  def created!(response)
@@ -306,6 +329,7 @@ module Sensu
306
329
  settings.cors.each do |header, value|
307
330
  headers["Access-Control-Allow-#{header}"] = value
308
331
  end
332
+ connected?
309
333
  protected! unless env["REQUEST_METHOD"] == "OPTIONS"
310
334
  end
311
335
 
@@ -342,27 +366,27 @@ module Sensu
342
366
  healthy << (info[:keepalives][:messages] <= max_messages)
343
367
  healthy << (info[:results][:messages] <= max_messages)
344
368
  end
345
- healthy.all? ? no_content! : unavailable!
369
+ healthy.all? ? no_content! : precondition_failed!
346
370
  end
347
371
  else
348
- unavailable!
372
+ precondition_failed!
349
373
  end
350
374
  end
351
375
 
352
376
  apost "/clients/?" do
353
- rules = {
354
- :name => {:type => String, :nil_ok => false, :regex => /\A[\w\.-]+\z/},
355
- :address => {:type => String, :nil_ok => false},
356
- :subscriptions => {:type => Array, :nil_ok => false}
357
- }
358
- read_data(rules) do |data|
359
- data[:keepalives] = false
377
+ read_data do |data|
378
+ data[:keepalives] = data.fetch(:keepalives, false)
360
379
  data[:version] = VERSION
361
380
  data[:timestamp] = Time.now.to_i
362
- settings.redis.set("client:#{data[:name]}", Sensu::JSON.dump(data)) do
363
- settings.redis.sadd("clients", data[:name]) do
364
- created!(Sensu::JSON.dump(:name => data[:name]))
381
+ validator = Validators::Client.new
382
+ if validator.valid?(data)
383
+ settings.redis.set("client:#{data[:name]}", Sensu::JSON.dump(data)) do
384
+ settings.redis.sadd("clients", data[:name]) do
385
+ created!(Sensu::JSON.dump(:name => data[:name]))
386
+ end
365
387
  end
388
+ else
389
+ bad_request!
366
390
  end
367
391
  end
368
392
  end
@@ -582,102 +606,172 @@ module Sensu
582
606
  end
583
607
 
584
608
  aget "/aggregates/?" do
585
- response = Array.new
586
- settings.redis.smembers("aggregates") do |checks|
587
- unless checks.empty?
588
- checks.each_with_index do |check_name, index|
589
- settings.redis.smembers("aggregates:#{check_name}") do |aggregates|
590
- aggregates.reverse!
591
- aggregates.map! do |issued|
592
- issued.to_i
609
+ settings.redis.smembers("aggregates") do |aggregates|
610
+ aggregates.map! do |aggregate|
611
+ {:name => aggregate}
612
+ end
613
+ body Sensu::JSON.dump(aggregates)
614
+ end
615
+ end
616
+
617
+ aget %r{^/aggregates/([\w\.-]+)/?$} do |aggregate|
618
+ response = {
619
+ :clients => 0,
620
+ :checks => 0,
621
+ :results => {
622
+ :ok => 0,
623
+ :warning => 0,
624
+ :critical => 0,
625
+ :unknown => 0,
626
+ :total => 0,
627
+ :stale => 0
628
+ }
629
+ }
630
+ settings.redis.smembers("aggregates:#{aggregate}") do |aggregate_members|
631
+ unless aggregate_members.empty?
632
+ clients = []
633
+ checks = []
634
+ results = []
635
+ aggregate_members.each_with_index do |member, index|
636
+ client_name, check_name = member.split(":")
637
+ clients << client_name
638
+ checks << check_name
639
+ result_key = "result:#{client_name}:#{check_name}"
640
+ settings.redis.get(result_key) do |result_json|
641
+ unless result_json.nil?
642
+ results << Sensu::JSON.load(result_json)
643
+ else
644
+ settings.redis.srem("aggregates:#{aggregate}", member)
593
645
  end
594
- item = {
595
- :check => check_name,
596
- :issued => aggregates
597
- }
598
- response << item
599
- if index == checks.length - 1
646
+ if index == aggregate_members.length - 1
647
+ response[:clients] = clients.uniq.length
648
+ response[:checks] = checks.uniq.length
649
+ max_age = integer_parameter(params[:max_age])
650
+ if max_age
651
+ result_count = results.length
652
+ timestamp = Time.now.to_i - max_age
653
+ results.reject! do |result|
654
+ result[:executed] < timestamp
655
+ end
656
+ response[:results][:stale] = result_count - results.length
657
+ end
658
+ response[:results][:total] = results.length
659
+ results.each do |result|
660
+ severity = (SEVERITIES[result[:status]] || "unknown")
661
+ response[:results][severity.to_sym] += 1
662
+ end
600
663
  body Sensu::JSON.dump(response)
601
664
  end
602
665
  end
603
666
  end
604
667
  else
605
- body Sensu::JSON.dump(response)
668
+ not_found!
606
669
  end
607
670
  end
608
671
  end
609
672
 
610
- aget %r{^/aggregates/([\w\.-]+)/?$} do |check_name|
611
- settings.redis.smembers("aggregates:#{check_name}") do |aggregates|
612
- unless aggregates.empty?
613
- aggregates.reverse!
614
- aggregates.map! do |issued|
615
- issued.to_i
616
- end
617
- age = integer_parameter(params[:age])
618
- if age
619
- timestamp = Time.now.to_i - age
620
- aggregates.reject! do |issued|
621
- issued > timestamp
673
+ adelete %r{^/aggregates/([\w\.-]+)/?$} do |aggregate|
674
+ settings.redis.smembers("aggregates") do |aggregates|
675
+ if aggregates.include?(aggregate)
676
+ settings.redis.srem("aggregates", aggregate) do
677
+ settings.redis.del("aggregates:#{aggregate}") do
678
+ no_content!
622
679
  end
623
680
  end
624
- body Sensu::JSON.dump(pagination(aggregates))
625
681
  else
626
682
  not_found!
627
683
  end
628
684
  end
629
685
  end
630
686
 
631
- adelete %r{^/aggregates/([\w\.-]+)/?$} do |check_name|
632
- settings.redis.smembers("aggregates:#{check_name}") do |aggregates|
633
- unless aggregates.empty?
634
- aggregates.each do |check_issued|
635
- result_set = "#{check_name}:#{check_issued}"
636
- settings.redis.del("aggregation:#{result_set}")
637
- settings.redis.del("aggregate:#{result_set}")
687
+ aget %r{^/aggregates/([\w\.-]+)/clients/?$} do |aggregate|
688
+ response = Array.new
689
+ settings.redis.smembers("aggregates:#{aggregate}") do |aggregate_members|
690
+ unless aggregate_members.empty?
691
+ clients = Hash.new
692
+ aggregate_members.each do |member|
693
+ client_name, check_name = member.split(":")
694
+ clients[client_name] ||= []
695
+ clients[client_name] << check_name
638
696
  end
639
- settings.redis.del("aggregates:#{check_name}") do
640
- settings.redis.srem("aggregates", check_name) do
641
- no_content!
642
- end
697
+ clients.each do |client_name, checks|
698
+ response << {
699
+ :name => client_name,
700
+ :checks => checks
701
+ }
643
702
  end
703
+ body Sensu::JSON.dump(response)
644
704
  else
645
705
  not_found!
646
706
  end
647
707
  end
648
708
  end
649
709
 
650
- aget %r{^/aggregates?/([\w\.-]+)/([\w\.-]+)/?$} do |check_name, check_issued|
651
- result_set = "#{check_name}:#{check_issued}"
652
- settings.redis.hgetall("aggregate:#{result_set}") do |aggregate|
653
- unless aggregate.empty?
654
- response = aggregate.inject(Hash.new) do |totals, (status, count)|
655
- totals[status] = Integer(count)
656
- totals
710
+ aget %r{^/aggregates/([\w\.-]+)/checks/?$} do |aggregate|
711
+ response = Array.new
712
+ settings.redis.smembers("aggregates:#{aggregate}") do |aggregate_members|
713
+ unless aggregate_members.empty?
714
+ checks = Hash.new
715
+ aggregate_members.each do |member|
716
+ client_name, check_name = member.split(":")
717
+ checks[check_name] ||= []
718
+ checks[check_name] << client_name
657
719
  end
658
- settings.redis.hgetall("aggregation:#{result_set}") do |results|
659
- parsed_results = results.inject(Array.new) do |parsed, (client_name, check_json)|
660
- check = Sensu::JSON.load(check_json)
661
- parsed << check.merge(:client => client_name)
662
- end
663
- if params[:summarize]
664
- options = params[:summarize].split(",")
665
- if options.include?("output")
666
- outputs = Hash.new(0)
667
- parsed_results.each do |result|
668
- outputs[result[:output]] += 1
720
+ checks.each do |check_name, clients|
721
+ response << {
722
+ :name => check_name,
723
+ :clients => clients
724
+ }
725
+ end
726
+ body Sensu::JSON.dump(response)
727
+ else
728
+ not_found!
729
+ end
730
+ end
731
+ end
732
+
733
+ aget %r{^/aggregates/([\w\.-]+)/results/([\w\.-]+)/?$} do |aggregate, severity|
734
+ response = Array.new
735
+ if SEVERITIES.include?(severity)
736
+ settings.redis.smembers("aggregates:#{aggregate}") do |aggregate_members|
737
+ unless aggregate_members.empty?
738
+ summaries = Hash.new
739
+ max_age = integer_parameter(params[:max_age])
740
+ current_timestamp = Time.now.to_i
741
+ aggregate_members.each_with_index do |member, index|
742
+ client_name, check_name = member.split(":")
743
+ result_key = "result:#{client_name}:#{check_name}"
744
+ settings.redis.get(result_key) do |result_json|
745
+ unless result_json.nil?
746
+ result = Sensu::JSON.load(result_json)
747
+ if SEVERITIES[result[:status]] == severity &&
748
+ (max_age.nil? || result[:executed] >= (current_timestamp - max_age))
749
+ summaries[check_name] ||= {}
750
+ summaries[check_name][result[:output]] ||= {:total => 0, :clients => []}
751
+ summaries[check_name][result[:output]][:total] += 1
752
+ summaries[check_name][result[:output]][:clients] << client_name
753
+ end
754
+ end
755
+ if index == aggregate_members.length - 1
756
+ summaries.each do |check_name, outputs|
757
+ summary = outputs.map do |output, output_summary|
758
+ {:output => output}.merge(output_summary)
759
+ end
760
+ response << {
761
+ :check => check_name,
762
+ :summary => summary
763
+ }
764
+ end
765
+ body Sensu::JSON.dump(response)
669
766
  end
670
- response[:outputs] = outputs
671
767
  end
672
768
  end
673
- if params[:results]
674
- response[:results] = parsed_results
675
- end
676
- body Sensu::JSON.dump(response)
769
+ else
770
+ not_found!
677
771
  end
678
- else
679
- not_found!
680
772
  end
773
+ else
774
+ bad_request!
681
775
  end
682
776
  end
683
777
 
@@ -0,0 +1,37 @@
1
+ require "sensu/settings/rules"
2
+ require "sensu/settings/validators/client"
3
+
4
+ module Sensu
5
+ module API
6
+ module Validators
7
+ # The error class for validation.
8
+ class Invalid < RuntimeError; end
9
+
10
+ class Client
11
+ # Include Sensu Settings rules and client validator.
12
+ include Sensu::Settings::Rules
13
+ include Sensu::Settings::Validators::Client
14
+
15
+ # Determine if a client definition is valid.
16
+ #
17
+ # @param client [Hash]
18
+ # @return [TrueClass, FalseClass]
19
+ def valid?(client)
20
+ validate_client(client)
21
+ true
22
+ rescue Invalid
23
+ false
24
+ end
25
+
26
+ private
27
+
28
+ # This method is called when `validate_client()` encounters an
29
+ # invalid definition object. This method raises an exception
30
+ # to be caught by `valid?()`.
31
+ def invalid(*arguments)
32
+ raise Invalid
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
data/lib/sensu/cli.rb CHANGED
@@ -26,6 +26,9 @@ module Sensu
26
26
  opts.on("-d", "--config_dir DIR[,DIR]", "DIR or comma-delimited DIR list for Sensu JSON config files") do |dir|
27
27
  options[:config_dirs] = dir.split(",")
28
28
  end
29
+ opts.on("--validate_config", "Validate the compiled configuration and exit") do
30
+ options[:validate_config] = true
31
+ end
29
32
  opts.on("-P", "--print_config", "Print the compiled configuration and exit") do
30
33
  options[:print_config] = true
31
34
  end
@@ -99,12 +99,40 @@ module Sensu
99
99
  end
100
100
  end
101
101
 
102
+ # Perform token substitution for an object. String values are
103
+ # passed to `substitute_tokens()`, arrays and sub-hashes are
104
+ # processed recursively. Numeric values are ignored.
105
+ #
106
+ # @param object [Object]
107
+ # @return [Array] containing the updated object with substituted
108
+ # values and an array of unmatched tokens.
109
+ def object_substitute_tokens(object)
110
+ unmatched_tokens = []
111
+ case object
112
+ when Hash
113
+ object.each do |key, value|
114
+ object[key], unmatched = object_substitute_tokens(value)
115
+ unmatched_tokens.push(*unmatched)
116
+ end
117
+ when Array
118
+ object.map! do |value|
119
+ value, unmatched = object_substitute_tokens(value)
120
+ unmatched_tokens.push(*unmatched)
121
+ value
122
+ end
123
+ when String
124
+ object, unmatched_tokens = substitute_tokens(object, @settings[:client])
125
+ end
126
+ [object, unmatched_tokens.uniq]
127
+ end
128
+
102
129
  # Execute a check command, capturing its output (STDOUT/ERR),
103
130
  # exit status code, execution duration, timestamp, and publish
104
131
  # the result. This method guards against multiple executions for
105
- # the same check. Check command tokens are substituted with the
106
- # associated client attribute values. If there are unmatched
107
- # check command tokens, the check command will not be executed,
132
+ # the same check. Check attribute value tokens are substituted
133
+ # with the associated client attribute values, via
134
+ # `object_substitute_tokens()`. If there are unmatched check
135
+ # attribute value tokens, the check will not be executed,
108
136
  # instead a check result will be published reporting the
109
137
  # unmatched tokens.
110
138
  #
@@ -113,11 +141,11 @@ module Sensu
113
141
  @logger.debug("attempting to execute check command", :check => check)
114
142
  unless @checks_in_progress.include?(check[:name])
115
143
  @checks_in_progress << check[:name]
116
- command, unmatched_tokens = substitute_tokens(check[:command], @settings[:client])
144
+ check, unmatched_tokens = object_substitute_tokens(check)
117
145
  if unmatched_tokens.empty?
118
- check[:executed] = Time.now.to_i
119
146
  started = Time.now.to_f
120
- Spawn.process(command, :timeout => check[:timeout]) do |output, status|
147
+ check[:executed] = started.to_i
148
+ Spawn.process(check[:command], :timeout => check[:timeout]) do |output, status|
121
149
  check[:duration] = ("%.3f" % (Time.now.to_f - started)).to_f
122
150
  check[:output] = output
123
151
  check[:status] = status
@@ -1,7 +1,7 @@
1
1
  module Sensu
2
2
  unless defined?(Sensu::VERSION)
3
3
  # Sensu release version.
4
- VERSION = "0.23.3".freeze
4
+ VERSION = "0.24.0.beta".freeze
5
5
 
6
6
  # Sensu check severities.
7
7
  SEVERITIES = %w[ok warning critical unknown].freeze