portertech-sensu-settings 10.18.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.
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