Module: Sensu::Utilities

Constant Summary collapse

EVAL_PREFIX =
"eval:".freeze
NANOSECOND_RESOLUTION =
9.freeze

Instance Method Summary collapse

Instance Method Details

#attributes_match?(object, match_attributes, support_eval = true, object_attributes = nil) ⇒ TrueClass, FalseClass

Determine if all attribute values match those of the corresponding object attributes. Attributes match if the value objects are equivalent, are both hashes with matching key/value pairs (recursive), have equal string values, or evaluate to true (Ruby eval).

Parameters:

  • object (Hash)
  • match_attributes (Object)
  • support_eval (TrueClass, FalseClass) (defaults to: true)
  • object_attributes (Object) (defaults to: nil)

Returns:

  • (TrueClass, FalseClass)


305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
# File 'lib/sensu/utilities.rb', line 305

def attributes_match?(object, match_attributes, support_eval=true, object_attributes=nil)
  object_attributes ||= object
  match_attributes.all? do |key, value_one|
    value_two = object_attributes[key]
    case
    when value_one == value_two
      true
    when value_one.is_a?(Hash) && value_two.is_a?(Hash)
      attributes_match?(object, value_one, support_eval, value_two)
    when value_one.to_s == value_two.to_s
      true
    when value_one.is_a?(String) && value_one.start_with?(EVAL_PREFIX) && support_eval
      eval_attribute_value(object, value_one, value_two)
    else
      false
    end
  end
end

#check_subdued?(check) ⇒ TrueClass, FalseClass

Determine if a check is subdued, by conditions set in the check definition. If any of the conditions are true, without an exception, the check is subdued.

Parameters:

  • check (Hash)

    definition.

Returns:

  • (TrueClass, FalseClass)


380
381
382
383
384
385
386
# File 'lib/sensu/utilities.rb', line 380

def check_subdued?(check)
  if check[:subdue]
    in_time_windows?(check[:subdue])
  else
    false
  end
end

#deep_dup(obj) ⇒ obj

Creates a deep dup of basic ruby objects with support for walking hashes and arrays.

Parameters:

  • obj (Object)

Returns:

  • (obj)

    a dup of the original object.



60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
# File 'lib/sensu/utilities.rb', line 60

def deep_dup(obj)
  if obj.class == Hash
    new_obj = obj.dup
    new_obj.each do |key, value|
      new_obj[deep_dup(key)] = deep_dup(value)
    end
    new_obj
  elsif obj.class == Array
    arr = []
    obj.each do |item|
      arr << deep_dup(item)
    end
    arr
  elsif obj.class == String
    obj.dup
  else
    obj
  end
end

#deep_merge(hash_one, hash_two) ⇒ Hash

Deep merge two hashes. Nested hashes are deep merged, arrays are concatenated and duplicate array items are removed.

Parameters:

  • hash_one (Hash)
  • hash_two (Hash)

Returns:

  • (Hash)

    deep merged hash.



40
41
42
43
44
45
46
47
48
49
50
51
52
53
# File 'lib/sensu/utilities.rb', line 40

def deep_merge(hash_one, hash_two)
  merged = hash_one.dup
  hash_two.each do |key, value|
    merged[key] = case
    when hash_one[key].is_a?(Hash) && value.is_a?(Hash)
      deep_merge(hash_one[key], value)
    when hash_one[key].is_a?(Array) && value.is_a?(Array)
      (hash_one[key] + value).uniq
    else
      value
    end
  end
  merged
end

#determine_check_cron_time(check) ⇒ Object

Determine the next check cron time.

Parameters:

  • check (Hash)

    definition.



391
392
393
394
395
396
# File 'lib/sensu/utilities.rb', line 391

def determine_check_cron_time(check)
  cron_parser = CronParser.new(check[:cron])
  current_time = Time.now
  next_cron_time = cron_parser.next(current_time)
  next_cron_time - current_time
end

#eval_attribute_value(object, raw_eval_string, raw_value) ⇒ TrueClass, FalseClass

Ruby ‘eval()` a string containing an expression, within the scope/context of a sandbox. This method is for attribute values starting with “eval:”, with the Ruby expression following the colon. A single variable is provided to the expression, `value`, equal to the corresponding object attribute value. Dot notation tokens in the expression, e.g. `:::mysql.user:::`, are substituted with the corresponding object attribute values prior to evaluation. The expression is expected to return a boolean value.

Parameters:

  • object (Hash)
  • raw_eval_string (String)

    containing the Ruby expression to be evaluated.

  • raw_value (Object)

    of the corresponding object attribute value.

Returns:

  • (TrueClass, FalseClass)


274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
# File 'lib/sensu/utilities.rb', line 274

def eval_attribute_value(object, raw_eval_string, raw_value)
  eval_string = process_eval_string(object, raw_eval_string)
  unless eval_string.nil?
    begin
      value = Marshal.load(Marshal.dump(raw_value))
      !!Sandbox.eval(eval_string, value)
    rescue StandardError, SyntaxError => error
      @logger.error("attribute value eval error", {
        :object => object,
        :raw_eval_string => raw_eval_string,
        :raw_value => raw_value,
        :error => error.to_s
      })
      false
    end
  else
    false
  end
end

#find_attribute_value(tree, path, default) ⇒ Object

Traverse a hash for an attribute value, with a fallback default value if nil.

Parameters:

  • tree (Hash)

    to traverse.

  • path (Array)

    of attribute keys.

  • default (Object)

    value if attribute value is nil.

Returns:

  • (Object)

    attribute or fallback default value.



169
170
171
172
173
174
175
176
# File 'lib/sensu/utilities.rb', line 169

def find_attribute_value(tree, path, default)
  attribute = tree[path.shift]
  if attribute.is_a?(Hash)
    find_attribute_value(attribute, path, default)
  else
    attribute.nil? ? default : attribute
  end
end

#in_time_window?(condition) ⇒ TrueClass, FalseClass

Determine if the current time falls within a time window. The provided condition must have a ‘:begin` and `:end` time, eg. “11:30:00 PM”, or `false` will be returned.

Parameters:

  • condition (Hash)

Options Hash (condition):

  • :begin (String)

    time.

  • :end (String)

    time.

Returns:

  • (TrueClass, FalseClass)


332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
# File 'lib/sensu/utilities.rb', line 332

def in_time_window?(condition)
  if condition.has_key?(:begin) && condition.has_key?(:end)
    begin_time = Time.parse(condition[:begin])
    end_time = Time.parse(condition[:end])
    if end_time < begin_time
      if Time.now < end_time
        begin_time = Time.parse(*begin_time.strftime("%Y-%m-%d 00:00:00.#{Array.new(NANOSECOND_RESOLUTION, 0).join} %:z"))
      else
        end_time = Time.parse(*end_time.strftime("%Y-%m-%d 23:59:59.#{Array.new(NANOSECOND_RESOLUTION, 9).join} %:z"))
      end
    end
    Time.now >= begin_time && Time.now <= end_time
  else
    false
  end
end

#in_time_windows?(conditions) ⇒ TrueClass, FalseClass

Determine if time window conditions for one or more days of the week are met. If a day of the week is provided, it can provide one or more conditions, each with a ‘:begin` and `:end` time, eg. “11:30:00 PM”, or `false` will be returned.

Parameters:

  • conditions (Hash)

Options Hash (conditions):

  • :days (String)

    of the week.

Returns:

  • (TrueClass, FalseClass)


357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
# File 'lib/sensu/utilities.rb', line 357

def in_time_windows?(conditions)
  in_window = false
  window_days = conditions[:days] || {}
  if window_days[:all]
    in_window = window_days[:all].any? do |condition|
      in_time_window?(condition)
    end
  end
  current_day = Time.now.strftime("%A").downcase.to_sym
  if !in_window && window_days[current_day]
    in_window = window_days[current_day].any? do |condition|
      in_time_window?(condition)
    end
  end
  in_window
end

#object_substitute_tokens(object, attributes) ⇒ Array

Perform token substitution for an object. String values are passed to ‘substitute_tokens()`, arrays and sub-hashes are processed recursively. Numeric values are ignored.

Parameters:

  • object (Object)
  • attributes (Hash)

Returns:

  • (Array)

    containing the updated object with substituted values and an array of unmatched tokens.



213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
# File 'lib/sensu/utilities.rb', line 213

def object_substitute_tokens(object, attributes)
  unmatched_tokens = []
  case object
  when Hash
    object.each do |key, value|
      object[key], unmatched = object_substitute_tokens(value, attributes)
      unmatched_tokens.push(*unmatched)
    end
  when Array
    object.map! do |value|
      value, unmatched = object_substitute_tokens(value, attributes)
      unmatched_tokens.push(*unmatched)
      value
    end
  when String
    object, unmatched_tokens = substitute_tokens(object, attributes)
  end
  [object, unmatched_tokens.uniq]
end

#process_cpu_times(&callback) ⇒ Array

Retrieve the process CPU times. If the cpu times cannot be determined and an error is thrown, ‘[nil, nil, nil, nil]` will be returned.

Returns:

  • (Array)

    CPU times: utime, stime, cutime, cstime



104
105
106
107
108
109
# File 'lib/sensu/utilities.rb', line 104

def process_cpu_times(&callback)
  determine_cpu_times = Proc.new do
    ::Process.times.to_a rescue [nil, nil, nil, nil]
  end
  EM::defer(determine_cpu_times, callback)
end

#process_eval_string(object, raw_eval_string) ⇒ String

Process an eval attribute value, a Ruby ‘eval()` string containing an expression to be evaluated within the scope/context of a sandbox. This methods strips away the expression prefix, `eval:`, and substitues any dot notation tokens with the corresponding event data values. If there are unmatched tokens, this method will return `nil`.

Returns:

  • (String)

    processed eval string.



243
244
245
246
247
248
249
250
251
252
253
254
255
256
# File 'lib/sensu/utilities.rb', line 243

def process_eval_string(object, raw_eval_string)
  eval_string = raw_eval_string.slice(5..-1)
  eval_string, unmatched_tokens = substitute_tokens(eval_string, object)
  if unmatched_tokens.empty?
    eval_string
  else
    @logger.error("attribute value eval unmatched tokens", {
      :object => object,
      :raw_eval_string => raw_eval_string,
      :unmatched_tokens => unmatched_tokens
    })
    nil
  end
end

#random_uuidString

Generate a random universally unique identifier.

Returns:

  • (String)

    random UUID.



114
115
116
# File 'lib/sensu/utilities.rb', line 114

def random_uuid
  ::SecureRandom.uuid
end

#redact_sensitive(obj, keys = nil) ⇒ Hash

Remove sensitive information from a hash (eg. passwords). By default, hash values will be redacted for the following keys: password, passwd, pass, api_key, api_token, access_key, secret_key, private_key, secret

Parameters:

  • obj (Object)

    to redact sensitive value from.

  • keys (Array) (defaults to: nil)

    that indicate sensitive values.

Returns:

  • (Hash)

    hash with redacted sensitive values.



126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
# File 'lib/sensu/utilities.rb', line 126

def redact_sensitive(obj, keys=nil)
  keys ||= %w[
    password passwd pass
    api_key api_token
    access_key secret_key private_key
    secret
    routing_key
    access_token_read access_token_write access_token_path
    webhook_url
    nickserv_password channel_password
    community
    keystore_password truststore_password
    proxy_password
    access_key_id secret_access_key
  ]
  obj = obj.dup
  if obj.is_a?(Hash)
    obj.each do |key, value|
      if keys.include?(key.to_s)
        obj[key] = "REDACTED"
      elsif value.is_a?(Hash) || value.is_a?(Array)
        obj[key] = redact_sensitive(value, keys)
      end
    end
  elsif obj.is_a?(Array)
    obj.map! do |item|
      if item.is_a?(Hash) || item.is_a?(Array)
        redact_sensitive(item, keys)
      else
        item
      end
    end
  end
  obj
end

#retry_until_true(wait = 0.5, &block) ⇒ Object

Retry a code block until it retures true. The first attempt and following retries are delayed.

Parameters:

  • wait (Numeric) (defaults to: 0.5)

    time to delay block calls.

  • block (Proc)

    to call that needs to return true.



26
27
28
29
30
31
32
# File 'lib/sensu/utilities.rb', line 26

def retry_until_true(wait=0.5, &block)
  EM::Timer.new(wait) do
    unless block.call
      retry_until_true(wait, &block)
    end
  end
end

#substitute_tokens(tokens, attributes) ⇒ Array

Substitute dot notation tokens (eg. :::db.name|production:::) with the associated definition attribute value. Tokens can provide a fallback default value, following a pipe.

Parameters:

  • tokens (String)
  • attributes (Hash)

Returns:

  • (Array)

    containing the string with tokens substituted and an array of unmatched tokens.



186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
# File 'lib/sensu/utilities.rb', line 186

def substitute_tokens(tokens, attributes)
  unmatched_tokens = []
  encoded_tokens = tokens.encode("UTF-8", "binary", {
    :invalid => :replace,
    :undef => :replace,
    :replace => ""
  })
  substituted = encoded_tokens.gsub(/:::([^:].*?):::/) do
    token, default = $1.to_s.split("|", 2)
    path = token.split(".").map(&:to_sym)
    matched = find_attribute_value(attributes, path, default)
    if matched.nil?
      unmatched_tokens << token
    end
    matched
  end
  [substituted, unmatched_tokens]
end

#system_addressString

Retrieve the system IP address. If a valid non-loopback IPv4 address cannot be found and an error is thrown, ‘nil` will be returned.

Returns:

  • (String)

    system ip address



93
94
95
96
97
# File 'lib/sensu/utilities.rb', line 93

def system_address
  ::Socket.ip_address_list.find { |address|
    address.ipv4? && !address.ipv4_loopback?
  }.ip_address rescue nil
end

#system_hostnameString

Retrieve the system hostname. If the hostname cannot be determined and an error is thrown, ‘nil` will be returned.

Returns:

  • (String)

    system hostname.



84
85
86
# File 'lib/sensu/utilities.rb', line 84

def system_hostname
  ::Socket.gethostname rescue nil
end

#testing?TrueClass, FalseClass

Determine if Sensu is being tested, using the process name. Sensu is being test if the process name is “rspec”,

Returns:

  • (TrueClass, FalseClass)


17
18
19
# File 'lib/sensu/utilities.rb', line 17

def testing?
  File.basename($0) == "rspec"
end