portertech-sensu-settings 10.18.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 4153f6c86a09ed8235ac34d06f7d869ddab639ed09db776dff89f764fe99377b
4
+ data.tar.gz: 32db3e0c760c611f630895d2eadc7e90665d6190e050c84ca56560286b6cd055
5
+ SHA512:
6
+ metadata.gz: e512898250001641dcda1b26bdcb740939c6e900804b134e0713fba0ef47cda55f85b3e21cfdd6173edeafa472e16bf2dffacf68bbce65483be5074959c38086
7
+ data.tar.gz: b23dc848b0b8884192f45c13d08e692eb49182c0034a12f0d59263426bf1258cf02d09d994e1c79f01692a578d69587c04f1a9275d1d3d03a2b806b3c68856f5
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2014 Heavy Water Operations, LLC.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,28 @@
1
+ # Sensu::Settings
2
+
3
+ [![Build Status](https://travis-ci.org/sensu/sensu-settings.svg?branch=master)](https://travis-ci.org/sensu/sensu-settings)
4
+ ![Gem Version](https://img.shields.io/gem/v/sensu-settings.svg)
5
+ ![MIT Licensed](https://img.shields.io/github/license/sensu/sensu.svg)
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ gem 'sensu-settings'
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ ## Usage
18
+
19
+ Documentation can be found [here](http://rubydoc.info/github/sensu/sensu-settings/Sensu/Settings).
20
+
21
+ ## Contributing
22
+
23
+ 0. By contributing to this project you agree to abide by the [code of conduct](https://sensuapp.org/conduct).
24
+ 1. [Fork it](https://github.com/sensu/sensu-settings/fork)
25
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
26
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
27
+ 4. Push to the branch (`git push origin my-new-feature`)
28
+ 5. Create a new Pull Request
@@ -0,0 +1,5 @@
1
+ module Sensu
2
+ module Settings
3
+ CATEGORIES = [:checks, :filters, :mutators, :handlers, :extensions]
4
+ end
5
+ end
@@ -0,0 +1,502 @@
1
+ require "sensu/settings/validator"
2
+ require "sensu/json"
3
+ require "tmpdir"
4
+ require "socket"
5
+ require "digest"
6
+
7
+ module Sensu
8
+ module Settings
9
+ class Loader
10
+ class Error < RuntimeError; end
11
+
12
+ # @!attribute [r] warnings
13
+ # @return [Array] loader warnings.
14
+ attr_reader :warnings
15
+
16
+ # @!attribute [r] errors
17
+ # @return [Array] loader errors.
18
+ attr_reader :errors
19
+
20
+ # @!attribute [r] loaded_files
21
+ # @return [Array] loaded config files.
22
+ attr_reader :loaded_files
23
+
24
+ def initialize
25
+ @warnings = []
26
+ @errors = []
27
+ @settings = default_settings
28
+ @indifferent_access = false
29
+ @loaded_files = []
30
+ self.class.create_category_methods
31
+ end
32
+
33
+ # Auto-detected defaults for client definition
34
+ #
35
+ # Client name defaults to system hostname.
36
+ # Client address defaults to first detected non-loopback ipv4 address.
37
+ #
38
+ # Client subscriptions are intentionally omitted here as sensu-client
39
+ # will provide defaults using client name after final settings are
40
+ # loaded.
41
+ #
42
+ # @return [Hash] default client settings
43
+ def client_defaults
44
+ {
45
+ :name => system_hostname,
46
+ :address => system_address
47
+ }
48
+ end
49
+
50
+ # Default settings.
51
+ #
52
+ # @return [Hash] settings.
53
+ def default_settings
54
+ default = {
55
+ :client => {},
56
+ :sensu => {
57
+ :spawn => {
58
+ :limit => 12
59
+ },
60
+ :keepalives => {
61
+ :thresholds => {
62
+ :warning => 120,
63
+ :critical => 180
64
+ }
65
+ }
66
+ },
67
+ :transport => {
68
+ :name => "rabbitmq",
69
+ :reconnect_on_error => true
70
+ }
71
+ }
72
+ CATEGORIES.each do |category|
73
+ default[category] = {}
74
+ end
75
+ if ["client", "rspec"].include?(sensu_service_name)
76
+ default[:client] = client_defaults
77
+ end
78
+ default
79
+ end
80
+
81
+ # Create setting category accessors and methods to test the
82
+ # existence of definitions. Called in initialize().
83
+ def self.create_category_methods
84
+ CATEGORIES.each do |category|
85
+ define_method(category) do
86
+ setting_category(category)
87
+ end
88
+ method_name = category.to_s.chop + "_exists?"
89
+ define_method(method_name.to_sym) do |name|
90
+ definition_exists?(category, name)
91
+ end
92
+ end
93
+ end
94
+
95
+ # Access settings as an indifferent hash.
96
+ #
97
+ # @return [Hash] settings.
98
+ def to_hash
99
+ unless @indifferent_access
100
+ indifferent_access!
101
+ @hexdigest = nil
102
+ end
103
+ @settings
104
+ end
105
+
106
+ # Retrieve the setting object corresponding to a key, acting
107
+ # like a Hash object.
108
+ #
109
+ # @param key [String, Symbol]
110
+ # @return [Object] value for key.
111
+ def [](key)
112
+ to_hash[key]
113
+ end
114
+
115
+ # Create a SHA256 hex digest for the settings Hash object. The
116
+ # client definition scope is ignored when the current process is
117
+ # not a Sensu client, as it is essentially ignored and it will
118
+ # likely cause a sum mismatch between two Sensu service systems.
119
+ # This method will not recalculate the hex digest, unless the
120
+ # settings have been altered, determine by the values of
121
+ # `@hexdigest` and `@indifferent_access`.
122
+ #
123
+ # @return [String] SHA256 hex digest.
124
+ def hexdigest
125
+ if @hexdigest && @indifferent_access
126
+ @hexdigest
127
+ else
128
+ hash = case sensu_service_name
129
+ when "client", "rspec"
130
+ to_hash
131
+ else
132
+ to_hash.reject do |key, value|
133
+ key.to_s == "client"
134
+ end
135
+ end
136
+ @hexdigest = Digest::SHA256.hexdigest(hash.to_s)
137
+ end
138
+ end
139
+
140
+ # Load settings from the environment.
141
+ #
142
+ # Loads: SENSU_TRANSPORT_NAME, RABBITMQ_URL, REDIS_URL,
143
+ # SENSU_CLIENT_NAME, SENSU_CLIENT_ADDRESS
144
+ # SENSU_CLIENT_SUBSCRIPTIONS, SENSU_API_PORT
145
+ def load_env
146
+ load_transport_env
147
+ load_rabbitmq_env
148
+ load_redis_env
149
+ load_client_env
150
+ load_api_env
151
+ end
152
+
153
+ # Load settings from a JSON file.
154
+ #
155
+ # @param [String] file path.
156
+ # @param must_exist [TrueClass, FalseClass] if the file must
157
+ # exist and is readable.
158
+ def load_file(file, must_exist=true)
159
+ if File.file?(file) && File.readable?(file)
160
+ begin
161
+ warning("loading config file", :file => file)
162
+ contents = read_config_file(file)
163
+ config = contents.empty? ? {} : Sensu::JSON.load(contents)
164
+ merged = deep_merge(@settings, config)
165
+ unless @loaded_files.empty?
166
+ changes = deep_diff(@settings, merged)
167
+ warning("config file applied changes", {
168
+ :file => file,
169
+ :changes => changes
170
+ })
171
+ end
172
+ @settings = merged
173
+ @indifferent_access = false
174
+ @loaded_files << file
175
+ rescue Sensu::JSON::ParseError => error
176
+ load_error("config file must be valid json", {
177
+ :file => file,
178
+ :error => error.to_s
179
+ })
180
+ end
181
+ elsif must_exist
182
+ load_error("config file does not exist or is not readable", :file => file)
183
+ else
184
+ warning("config file does not exist or is not readable", :file => file)
185
+ warning("ignoring config file", :file => file)
186
+ end
187
+ end
188
+
189
+ # Load settings from files in a directory. Files may be in
190
+ # nested directories.
191
+ #
192
+ # @param [String] directory path.
193
+ def load_directory(directory)
194
+ warning("loading config files from directory", :directory => directory)
195
+ path = directory.gsub(/\\(?=\S)/, "/")
196
+ if File.readable?(path) && File.executable?(path)
197
+ Dir.glob(File.join(path, "**{,/*/**}/*.json")).uniq.each do |file|
198
+ load_file(file)
199
+ end
200
+ else
201
+ load_error("insufficient permissions for loading", :directory => directory)
202
+ end
203
+ end
204
+
205
+ # Load Sensu client settings overrides. This method adds any overrides to
206
+ # the client definition. Overrides include:
207
+ #
208
+ # * Ensuring client subscriptions include a single subscription based on the
209
+ # client name, e.g "client:i-424242".
210
+ def load_client_overrides
211
+ @settings[:client][:subscriptions] ||= []
212
+ if @settings[:client][:subscriptions].is_a?(Array)
213
+ @settings[:client][:subscriptions] << "client:#{@settings[:client][:name]}"
214
+ @settings[:client][:subscriptions].uniq!
215
+ warning("applied sensu client overrides", :client => @settings[:client])
216
+ @indifferent_access = false
217
+ else
218
+ warning("unable to apply sensu client overrides", {
219
+ :reason => "client subscriptions is not an array",
220
+ :client => @settings[:client]
221
+ })
222
+ end
223
+ end
224
+
225
+ # Load overrides, i.e. settings which should always be present.
226
+ # Examples include client settings overrides which ensure a per-client subscription.
227
+ def load_overrides!
228
+ load_client_overrides if ["client", "rspec"].include?(sensu_service_name)
229
+ end
230
+
231
+ # Set Sensu settings related environment variables. This method
232
+ # sets `SENSU_LOADED_TEMPFILE` to a new temporary file path,
233
+ # a file containing the colon delimited list of loaded
234
+ # configuration files (using `create_loaded_tempfile!()`. The
235
+ # environment variable `SENSU_CONFIG_FILES` has been removed,
236
+ # due to the exec ARG_MAX (E2BIG) error when spawning processes
237
+ # after loading many configuration files (e.g. > 2000).
238
+ def set_env!
239
+ ENV["SENSU_LOADED_TEMPFILE"] = create_loaded_tempfile!
240
+ end
241
+
242
+ # Validate the loaded settings.
243
+ #
244
+ # @return [Array] validation failures.
245
+ def validate
246
+ validator = Validator.new
247
+ @errors += validator.run(@settings, sensu_service_name)
248
+ end
249
+
250
+ private
251
+
252
+ # Retrieve setting category definitions.
253
+ #
254
+ # @param [Symbol] category to retrive.
255
+ # @return [Array<Hash>] category definitions.
256
+ def setting_category(category)
257
+ @settings[category].map do |name, details|
258
+ details.merge(:name => name.to_s)
259
+ end
260
+ end
261
+
262
+ # Check to see if a definition exists in a category.
263
+ #
264
+ # @param [Symbol] category to inspect for the definition.
265
+ # @param [String] name of definition.
266
+ # @return [TrueClass, FalseClass]
267
+ def definition_exists?(category, name)
268
+ @settings[category].has_key?(name.to_sym)
269
+ end
270
+
271
+ # Creates an indifferent hash.
272
+ #
273
+ # @return [Hash] indifferent hash.
274
+ def indifferent_hash
275
+ Hash.new do |hash, key|
276
+ if key.is_a?(String)
277
+ hash[key.to_sym]
278
+ end
279
+ end
280
+ end
281
+
282
+ # Create a copy of a hash with indifferent access.
283
+ #
284
+ # @param hash [Hash] hash to make indifferent.
285
+ # @return [Hash] indifferent version of hash.
286
+ def with_indifferent_access(hash)
287
+ hash = indifferent_hash.merge(hash)
288
+ hash.each do |key, value|
289
+ if value.is_a?(Hash)
290
+ hash[key] = with_indifferent_access(value)
291
+ end
292
+ end
293
+ end
294
+
295
+ # Update settings to have indifferent access.
296
+ def indifferent_access!
297
+ @settings = with_indifferent_access(@settings)
298
+ @indifferent_access = true
299
+ end
300
+
301
+ # Load Sensu transport settings from the environment. This
302
+ # method sets the Sensu transport name to `SENSU_TRANSPORT_NAME`
303
+ # if set.
304
+ def load_transport_env
305
+ if ENV["SENSU_TRANSPORT_NAME"]
306
+ @settings[:transport][:name] = ENV["SENSU_TRANSPORT_NAME"]
307
+ warning("using sensu transport name environment variable", :transport => @settings[:transport])
308
+ @indifferent_access = false
309
+ end
310
+ end
311
+
312
+ # Load Sensu RabbitMQ settings from the environment. This method
313
+ # sets the RabbitMQ settings to `RABBITMQ_URL` if set. The Sensu
314
+ # RabbitMQ transport accepts a URL string for options.
315
+ def load_rabbitmq_env
316
+ if ENV["RABBITMQ_URL"]
317
+ @settings[:rabbitmq] = ENV["RABBITMQ_URL"]
318
+ warning("using rabbitmq url environment variable", :rabbitmq => @settings[:rabbitmq])
319
+ @indifferent_access = false
320
+ end
321
+ end
322
+
323
+ # Load Sensu Redis settings from the environment.
324
+ #
325
+ # This method evaluates the REDIS_SENTINEL_URLS and REDIS_URL environment variables
326
+ # and configures the Redis settings accordingly.
327
+ #
328
+ # When REDIS_SENTINEL_URLS is provided as a list of one or more
329
+ # comma-separated URLs, e.g.
330
+ # "redis://10.0.0.1:26379,redis://10.0.0.2:26379" these URLs will take
331
+ # precedence over the value provided by REDIS_URL, if any.
332
+ #
333
+ # As the redis library accepts a URL string for options. This
334
+ # configuration applies to data storage and the redis transport, if used.
335
+ def load_redis_env
336
+ if ENV["REDIS_SENTINEL_URLS"]
337
+ @settings[:redis] = {:sentinels => ENV["REDIS_SENTINEL_URLS"]}
338
+ warning("using redis sentinel url environment variable", :sentinels => @settings[:redis][:sentinels])
339
+ @indifferent_access = false
340
+ elsif ENV["REDIS_URL"]
341
+ @settings[:redis] = ENV["REDIS_URL"]
342
+ warning("using redis url environment variable", :redis => @settings[:redis])
343
+ @indifferent_access = false
344
+ end
345
+ end
346
+
347
+ # Load Sensu client settings from the environment. This method
348
+ # loads client settings from several variables:
349
+ # `SENSU_CLIENT_NAME`, `SENSU_CLIENT_ADDRESS`, and
350
+ # `SENSU_CLIENT_SUBSCRIPTIONS`.
351
+ def load_client_env
352
+ @settings[:client][:name] = ENV["SENSU_CLIENT_NAME"] if ENV["SENSU_CLIENT_NAME"]
353
+ @settings[:client][:address] = ENV["SENSU_CLIENT_ADDRESS"] if ENV["SENSU_CLIENT_ADDRESS"]
354
+ @settings[:client][:subscriptions] = ENV["SENSU_CLIENT_SUBSCRIPTIONS"].split(",") if ENV["SENSU_CLIENT_SUBSCRIPTIONS"]
355
+ if ENV.keys.any? {|k| k =~ /^SENSU_CLIENT/}
356
+ warning("using sensu client environment variables", :client => @settings[:client])
357
+ end
358
+ @indifferent_access = false
359
+ end
360
+
361
+ # Load Sensu API settings from the environment. This method sets
362
+ # the API port to `SENSU_API_PORT` if set.
363
+ def load_api_env
364
+ if ENV["SENSU_API_PORT"]
365
+ @settings[:api] ||= {}
366
+ @settings[:api][:port] = ENV["SENSU_API_PORT"].to_i
367
+ warning("using api port environment variable", :api => @settings[:api])
368
+ @indifferent_access = false
369
+ end
370
+ end
371
+
372
+ # Read a configuration file and force its encoding to 8-bit
373
+ # ASCII, ignoring invalid characters. If there is a UTF-8 BOM,
374
+ # it will be removed. Some JSON parsers force ASCII but do not
375
+ # remove the UTF-8 BOM if present, causing encoding conversion
376
+ # errors. This method is for consistency across Sensu::JSON
377
+ # adapters and system platforms.
378
+ #
379
+ # @param [String] file path to read.
380
+ # @return [String] file contents.
381
+ def read_config_file(file)
382
+ contents = IO.read(file)
383
+ if contents.respond_to?(:force_encoding)
384
+ encoding = ::Encoding::ASCII_8BIT
385
+ contents = contents.force_encoding(encoding)
386
+ contents.sub!("\xEF\xBB\xBF".force_encoding(encoding), "")
387
+ else
388
+ contents.sub!(/^\357\273\277/, "")
389
+ end
390
+ contents.strip
391
+ end
392
+
393
+ # Deep merge two hashes.
394
+ #
395
+ # @param [Hash] hash_one to serve as base.
396
+ # @param [Hash] hash_two to merge in.
397
+ # @return [Hash] deep merged hash.
398
+ def deep_merge(hash_one, hash_two)
399
+ merged = hash_one.dup
400
+ hash_two.each do |key, value|
401
+ merged[key] = case
402
+ when hash_one[key].is_a?(Hash) && value.is_a?(Hash)
403
+ deep_merge(hash_one[key], value)
404
+ when hash_one[key].is_a?(Array) && value.is_a?(Array)
405
+ hash_one[key].concat(value).uniq
406
+ else
407
+ value
408
+ end
409
+ end
410
+ merged
411
+ end
412
+
413
+ # Compare two hashes.
414
+ #
415
+ # @param [Hash] hash_one to compare.
416
+ # @param [Hash] hash_two to compare.
417
+ # @return [Hash] comparison diff hash.
418
+ def deep_diff(hash_one, hash_two)
419
+ keys = hash_one.keys.concat(hash_two.keys).uniq
420
+ keys.inject(Hash.new) do |diff, key|
421
+ unless hash_one[key] == hash_two[key]
422
+ if hash_one[key].is_a?(Hash) && hash_two[key].is_a?(Hash)
423
+ diff[key] = deep_diff(hash_one[key], hash_two[key])
424
+ else
425
+ diff[key] = [hash_one[key], hash_two[key]]
426
+ end
427
+ end
428
+ diff
429
+ end
430
+ end
431
+
432
+ # Create a temporary file containing the colon delimited list of
433
+ # loaded configuration files. Ruby TempFile is not used to
434
+ # create the temporary file as it would be removed if the Sensu
435
+ # service daemonizes (fork/detach). The file is created in the
436
+ # system temporary file directory for the platform (Linux,
437
+ # Windows, etc.) and the file name contains the Sensu service
438
+ # name to reduce the likelihood of one Sensu service affecting
439
+ # another.
440
+ #
441
+ # @return [String] tempfile path.
442
+ def create_loaded_tempfile!
443
+ dir = ENV["SENSU_LOADED_TEMPFILE_DIR"] || Dir.tmpdir
444
+ file_name = "sensu_#{sensu_service_name}_loaded_files"
445
+ path = File.join(dir, file_name)
446
+ File.open(path, "w") do |file|
447
+ file.write(@loaded_files.join(":"))
448
+ end
449
+ path
450
+ end
451
+
452
+ # Retrieve Sensu service name.
453
+ #
454
+ # @return [String] service name.
455
+ def sensu_service_name
456
+ File.basename($0).split("-").last
457
+ end
458
+
459
+ # Retrieve the system hostname. If the hostname cannot be
460
+ # determined and an error is thrown, return "unknown", the same
461
+ # value Sensu uses for JIT clients.
462
+ #
463
+ # @return [String] system hostname.
464
+ def system_hostname
465
+ Socket.gethostname rescue "unknown"
466
+ end
467
+
468
+ # Retrieve the system IP address. If a valid non-loopback
469
+ # IPv4 address cannot be found and an error is thrown,
470
+ # "unknown" will be returned.
471
+ #
472
+ # @return [String] system ip address
473
+ def system_address
474
+ Socket.ip_address_list.find { |address|
475
+ address.ipv4? && !address.ipv4_loopback?
476
+ }.ip_address rescue "unknown"
477
+ end
478
+
479
+ # Record a warning.
480
+ #
481
+ # @param message [String] warning message.
482
+ # @param data [Hash] warning context.
483
+ # @return [Array] current warnings.
484
+ def warning(message, data={})
485
+ @warnings << {
486
+ :message => message
487
+ }.merge(data)
488
+ end
489
+
490
+ # Record a load error and raise a load error exception.
491
+ #
492
+ # @param message [String] load error message.
493
+ # @param data [Hash] load error context.
494
+ def load_error(message, data={})
495
+ @errors << {
496
+ :message => message
497
+ }.merge(data)
498
+ raise(Error, message)
499
+ end
500
+ end
501
+ end
502
+ end