onering-agent 0.4.3

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.
@@ -0,0 +1,360 @@
1
+ #!/usr/bin/env ruby
2
+ require 'rubygems'
3
+ require 'timeout'
4
+ require 'optparse'
5
+ require 'hashlib'
6
+ require 'set'
7
+
8
+ module Onering
9
+ class Reporter
10
+ DEFAULT_PLUGIN_GEMNAMES=[
11
+ 'onering-report'
12
+ ]
13
+
14
+ DEFAULT_PLUGIN_PATH = [
15
+ '/var/lib/onering/reporter'
16
+ ]
17
+
18
+ DEFAULT_FACTER_PATH = [
19
+ '/etc/facter'
20
+ ]
21
+
22
+ DEFAULT_CACHE_FILE='/var/tmp/.onering-report-cache.json'
23
+ DEFAULT_CACHE_MAXAGE=600
24
+
25
+ include Onering::Util
26
+
27
+ attr_reader :facter_path
28
+
29
+ class PluginDelegate
30
+ def initialize(reporter, options={})
31
+ @_name = options.get(:plugin)
32
+ @_path = options.get(:path)
33
+
34
+ Onering::Logger.debug3("Creating plugin delegate for plugin #{@_name}", "Onering::Reporter::PluginDelegate")
35
+ @_reporter = reporter
36
+ end
37
+
38
+ def get_binding()
39
+ return binding()
40
+ end
41
+
42
+ # DSL methods
43
+ # -------------------------------------------------------------------------
44
+ def report(&block)
45
+ if block_given?
46
+ start = Time.now.to_f
47
+ yield
48
+
49
+ finish = (Time.now.to_f - start.to_f)
50
+ finish = (finish.round(4) rescue finish)
51
+ Onering::Logger.debug3("Finished evaluating report for plugin #{@_name}, took #{finish} seconds", "Onering::Reporter::PluginDelegate")
52
+ end
53
+ end
54
+
55
+ def property(name, value=nil)
56
+ @_reporter.property(name, value)
57
+ end
58
+
59
+ def stat(name, value=nil)
60
+ unless value.nil?
61
+ @_reporter.property((['metrics']+name.to_s.split('.')).join('.'), value)
62
+ end
63
+ end
64
+ end
65
+
66
+ def initialize(config={})
67
+ @options = config
68
+ @facter_path = DEFAULT_FACTER_PATH
69
+ @detected_gems = []
70
+
71
+ @path = [*Onering::Config.get('reporter.plugin_path',[])]
72
+ @path += DEFAULT_PLUGIN_PATH
73
+
74
+
75
+ begin
76
+ specs = Set.new()
77
+ @detected_gems = []
78
+
79
+ Gem::Specification.each do |spec|
80
+ specs << spec.name
81
+ end
82
+
83
+ @detected_gems = (specs.to_a.select{|i|
84
+ i =~ /^onering-report-/
85
+ } - DEFAULT_PLUGIN_GEMNAMES)
86
+ rescue Exception => e
87
+ Onering::Logger.warn("Unable to detect plugin gems: #{e.class.name} - #{e.message}", "Onering::Reporter")
88
+ end
89
+
90
+ # add gem paths to the @path
91
+ ([*Onering::Config.get('reporter.plugin_gems',[])]+@detected_gems+DEFAULT_PLUGIN_GEMNAMES).compact.each do |g|
92
+ begin
93
+ p = File.join(Util.gem_path(g), 'lib')
94
+ @path << File.join(p, 'reporter')
95
+ @facter_path << File.join(p, 'facter')
96
+ rescue Gem::LoadError => e
97
+ Onering::Logger.warn("Error loading gem: #{e.message}", "Onering::Reporter")
98
+ next
99
+ end
100
+ end
101
+
102
+ begin
103
+ ENV['FACTERLIB'] = @facter_path.join(':')
104
+ require 'facter'
105
+ Onering::Logger.debug("Facter loaded successfully, FACTERLIB is #{ENV['FACTERLIB']}", "Onering::Reporter")
106
+
107
+ rescue LoadError
108
+ Onering::Logger.error("Unable to load Facter library", "Onering::Reporter")
109
+ end
110
+ end
111
+
112
+ def load_plugins()
113
+
114
+ # load plugins from @path
115
+ @path.compact.uniq.each do |root|
116
+ begin
117
+ Dir["#{root}/*"].uniq.each do |directory|
118
+
119
+ # only process top-level directories
120
+ if File.directory?(directory)
121
+ d = File.basename(directory)
122
+
123
+ Onering::Logger.debug("Loading plugins from path #{directory}", "Onering::Reporter")
124
+
125
+ # allow plugins to be conditionally loaded based on fact values:
126
+ # default - always load
127
+ # <fact>-<fact_value> - load if <fact> == <fact_value>
128
+ #
129
+ if d == 'default' or Facter.value(d.split('-',2).first).to_s.downcase.nil_empty == d.split('-',2).last.to_s.downcase.nil_empty
130
+
131
+ Dir[File.join(directory, '*.rb')].each do |plugin|
132
+ plugin = File.basename(plugin, '.rb')
133
+
134
+ begin
135
+ Timeout.timeout((@options[:plugin_timeout] || 10).to_i) do
136
+ Onering::Logger.debug("Loading plugin #{directory}/#{plugin}.rb", "Onering::Reporter")
137
+ Onering::Logger.debug3("Properties will be set in report object #{@_report.object_id}", "Onering::Reporter")
138
+ eval(File.read("#{directory}/#{plugin}.rb"), PluginDelegate.new(self, {
139
+ :plugin => plugin,
140
+ :path => "#{directory}/#{plugin}.rb"
141
+ }).get_binding())
142
+ end
143
+ rescue Timeout::Error
144
+ Onering::Logger.warn("Plugin #{plugin} took too long to return, skipping", "Onering::Reporter")
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end
150
+ rescue Exception => e
151
+ raise e if e.class === Timeout::Error
152
+
153
+ Onering::Logger.warn(e.message, "Onering::Reporter/#{e.class.name}")
154
+
155
+ e.backtrace.each do |eb|
156
+ Onering::Logger.debug(eb, "Onering::Reporter/#{e.class.name}")
157
+ end
158
+
159
+ next
160
+ end
161
+ end
162
+ end
163
+
164
+ def property(name, value=nil)
165
+ unless value.nil?
166
+ Onering::Logger.debug3("-> Set property #{name.to_s} (was: #{@_report[:properties].get(name.to_s,'null')}) in object #{@_report.object_id}", "Onering::Reporter")
167
+ @_report[:properties].set(name.to_s, value)
168
+ end
169
+ end
170
+
171
+ def report(options={})
172
+ options = @options.merge(options)
173
+ @id = (@options[:id] || Onering::Config.get('id') || Onering::Config.get('reporter.fields.id') || Onering::Util.fact('hardwareid', nil))
174
+
175
+ if not @id.nil?
176
+ if options[:nocache]
177
+ return _generated_report()
178
+ else
179
+ rv = _cached_report(options)
180
+ return _generated_report() if rv.nil? or rv.empty?
181
+ return rv
182
+ end
183
+ else
184
+ Onering::Logger.fatal!("Cannot generate report without an ID", "Onering::Reporter")
185
+ end
186
+
187
+ return {}
188
+ end
189
+
190
+
191
+ def get(field, default=nil, options={})
192
+ if options[:data].is_a?(Hash)
193
+ _report = options[:data]
194
+ else
195
+ _report = self.report(options)
196
+ end
197
+
198
+ # this is kinda ugly
199
+ # because we don't know which property might have an @-prefix, progressively
200
+ # search through all of them. first non-null match wins
201
+ parts = field.to_s.split('.')
202
+
203
+ # create an array with every component of the path prefixed with the @-symbol, then with
204
+ # the path as is.
205
+ #
206
+ # e.g.: onering report get metrics.disk.block
207
+ # -> value exists in the inventory as properties.metrics.disk.@block,
208
+ # but the user shouldn't need to know where that @-prefix is, so...
209
+ #
210
+ # Search for all of these, first non-nil value wins:
211
+ # * properties.metrics.disk.block
212
+ # * properties.@metrics.disk.block
213
+ # * properties.metrics.@disk.block
214
+ # * properties.metrics.disk.@block
215
+ # * metrics.disk.block
216
+ #
217
+ candidates = [(['properties']+parts).join('.')]
218
+
219
+ parts.each_index{|ix|
220
+ candidates << (['properties']+(ix == 0 ? [] : parts[0..(ix-1)]) + ["@#{parts[ix]}"] + parts[ix+1..-1]).join('.')
221
+ }.flatten()
222
+
223
+ rv = nil
224
+
225
+ # search for the key using science or something
226
+ candidates.each do |c|
227
+ rv = _report.get(c)
228
+ break unless rv.nil?
229
+ end
230
+
231
+ # if we're still nil by this point, use the fallback value
232
+ rv = _report.get(field) if rv.nil?
233
+
234
+ # attempt to get the value remotely if not found locally
235
+ if rv.nil? and not options[:local]
236
+ hid = Onering::Util.fact(:hardwareid)
237
+
238
+ if not hid.nil?
239
+ Onering::Logger.debug("Getting remote value #{field} for asset #{hid}")
240
+ return Onering::API.new(options[:api]).assets.get_field(hid, field, default)
241
+ end
242
+ end
243
+
244
+ return default if rv.nil?
245
+ return rv
246
+ end
247
+
248
+
249
+ def _generated_report()
250
+ Timeout.timeout((@options[:timeout] || 60).to_i) do
251
+ hostname = (Facter.value('fqdn') rescue %x{hostname -f}.strip.chomp)
252
+
253
+ @_report = {
254
+ :id => @id,
255
+ :name => hostname,
256
+ :aliases => @options[:aliases],
257
+ :tags => @options[:tags],
258
+ :status => (@options[:status] || 'online'),
259
+ :inventory => true,
260
+ :properties => {}
261
+ }
262
+
263
+ # loads plugins and populates @_report
264
+ load_plugins()
265
+
266
+ # pull report field overrides from the config file
267
+ Onering::Config.get('reporter.fields',{}).each do |key, value|
268
+ Onering::Logger.debug("Override value #{key} from config file", "Onering::CLI::Report")
269
+
270
+ if value.is_a?(Hash)
271
+ value.coalesce(key, nil, '.').each do |k,v|
272
+ v = nil if ['null', '', '-'].include?(v.to_s.strip.chomp)
273
+ @_report = @_report.set(k, v)
274
+ end
275
+ else
276
+ value = nil if ['null', '', '-'].include?(value.to_s.strip.chomp)
277
+ @_report = @_report.set(key, value)
278
+ end
279
+ end
280
+
281
+ # return final report
282
+ return @_report.stringify_keys()
283
+ end
284
+
285
+ return {}
286
+ end
287
+
288
+ def _cached_report(options={})
289
+ options = @options.merge(options)
290
+ cachefile = (options[:cachefile] || DEFAULT_CACHE_FILE)
291
+ tries = 0
292
+
293
+ catch(:retry) do
294
+ tries += 1
295
+
296
+ if tries > 10
297
+ Onering::Logger.error("Too many retries reading cache #{cachefile}, generating report", "Onering::Reporter")
298
+ return _generated_report()
299
+ end
300
+
301
+ if File.readable?(cachefile)
302
+ Onering::Logger.debug("Loading cache file at #{cachefile}", "Onering::Reporter")
303
+ cache = File.read(cachefile)
304
+ cache = (MultiJson.load(cache) rescue {})
305
+
306
+ if _cache_expired?(cache, options[:maxage])
307
+ Onering::Logger.debug("Cache expired, regenerating", "Onering::Reporter")
308
+ throw :retry if _update_cache_file(cachefile)
309
+ end
310
+
311
+ if options[:cacheregen] == true
312
+ Onering::Logger.debug("Forcing cache regeneration", "Onering::Reporter")
313
+ cache = _update_cache_file(cachefile)
314
+ end
315
+
316
+ if cache
317
+ # remove cached_at key
318
+ Onering::Logger.debug("Using cached data (#{Time.now.to_i - Time.parse(cache.get('cached_at')).to_i} seconds old)", "Onering::Reporter")
319
+ cache.delete('cached_at')
320
+ return cache
321
+ end
322
+ else
323
+ Onering::Logger.debug("Report cache file could not be read at #{cachefile}", "Onering::Reporter")
324
+ throw :retry if _update_cache_file(cachefile)
325
+ end
326
+ end
327
+
328
+ return {}
329
+ end
330
+
331
+
332
+ def _update_cache_file(cachefile=DEFAULT_CACHE_FILE)
333
+ begin
334
+ report = nil
335
+
336
+ File.open(cachefile, 'w+') do |file|
337
+ Onering::Logger.debug("Regenerating cache file at #{cachefile}", "Onering::Reporter")
338
+ report = _generated_report()
339
+ report['cached_at'] = Time.now.strftime('%Y-%m-%dT%H:%M:%S%z')
340
+ json = MultiJson.dump(report, :pretty => true)
341
+ file.puts(json)
342
+ end
343
+
344
+ return report
345
+ rescue Exception => e
346
+ Onering::Logger.info("Unable to write cache file #{cachefile}: #{e.class.name} - #{e.message}", "Onering::Reporter")
347
+ return false
348
+ end
349
+ end
350
+
351
+
352
+ def _cache_expired?(cache, age=DEFAULT_CACHE_MAXAGE)
353
+ if cache.is_a?(Hash)
354
+ return (Time.parse(cache.get('cached_at')) < (Time.now - age) rescue true)
355
+ else
356
+ return true
357
+ end
358
+ end
359
+ end
360
+ end
@@ -0,0 +1,150 @@
1
+ module Onering
2
+ module Util
3
+ module String
4
+ def nil_empty
5
+ return nil if (self.strip.chomp.empty? rescue true)
6
+ self.strip.chomp
7
+ end
8
+
9
+ def to_bytes
10
+ case self
11
+ when /^\s*([\d\.]+)\s*([KMGTPEZY]?)([Bb])\s*$/
12
+ power = case $2
13
+ when 'K' then 1
14
+ when 'M' then 2
15
+ when 'G' then 3
16
+ when 'T' then 4
17
+ when 'P' then 5
18
+ when 'E' then 6
19
+ when 'Z' then 7
20
+ when 'Y' then 8
21
+ else 0
22
+ end
23
+
24
+ num = (Integer($1) rescue Float($1))
25
+ div = ($3 == 'b' ? 8 : 1)
26
+
27
+ return ((num * (1024 ** power)) / div)
28
+ else
29
+ return nil
30
+ end
31
+ end
32
+ end
33
+
34
+ extend self
35
+
36
+
37
+ HTTP_STATUS_CODES = {
38
+ 400 => 'Bad Request',
39
+ 401 => 'Unauthorized',
40
+ 402 => 'Payment Required',
41
+ 403 => 'Forbidden',
42
+ 404 => 'Not Found',
43
+ 405 => 'Method Not Allowed',
44
+ 406 => 'Not Acceptable',
45
+ 407 => 'Proxy Authentication Required',
46
+ 408 => 'Request Timeout',
47
+ 409 => 'Conflict',
48
+ 410 => 'Gone',
49
+ 411 => 'Length Required',
50
+ 412 => 'Precondition Failed',
51
+ 413 => 'Request Entity Too Large',
52
+ 414 => 'Request-URI Too Long',
53
+ 415 => 'Unsupported Media Type',
54
+ 416 => 'Requested Range Not Satisfiable',
55
+ 417 => 'Expectation Failed',
56
+ 418 => 'I\'m a Teapot',
57
+ 420 => 'Enhance Your Calm',
58
+ 422 => 'Unprocessable Entity',
59
+ 423 => 'Locked',
60
+ 424 => 'Failed Dependency',
61
+ 426 => 'Upgrade Required',
62
+ 428 => 'Precondition Required',
63
+ 429 => 'Too Many Requests',
64
+ 431 => 'Request Header Fields Too Large',
65
+ 444 => 'No Response',
66
+ 451 => 'Unavailable For Legal Reasons',
67
+
68
+ 500 => 'Internal Server Error',
69
+ 501 => 'Not Implemented',
70
+ 502 => 'Bad Gateway',
71
+ 503 => 'Service Unavailable',
72
+ 504 => 'Gateway Timeout',
73
+ 505 => 'HTTP Version Not Supported',
74
+ 508 => 'Loop Detected',
75
+ 509 => 'Bandwidth Limit Exceeded',
76
+ 510 => 'Not Extended',
77
+ 511 => 'Network Authentication Required'
78
+ }
79
+
80
+ def gem_path(name)
81
+ if Gem::Specification.respond_to?(:find_by_name)
82
+ return Gem::Specification.find_by_name(name).gem_dir
83
+ else
84
+ return Gem::SourceIndex.from_installed_gems.find_name(name).sort{|a,b|
85
+ a.version.to_s <=> b.version.to_s
86
+ }.last.full_gem_path
87
+ end
88
+ end
89
+
90
+ def fact(name, default=nil)
91
+ reporter = Onering::Reporter.new()
92
+ name = name.to_s
93
+
94
+ if defined?(Facter)
95
+ if name.downcase == 'all'
96
+ return Facter.to_hash()
97
+ else
98
+ fact_value = Facter.value(name)
99
+
100
+ # short circuit nil responses
101
+ return default if fact_value.nil?
102
+
103
+ # if we are asking for a nested object...
104
+ if name.include?('.')
105
+ # ...and the response IS an object...
106
+ if fact_value.is_a?(Hash)
107
+ # remove the first part and return the rest
108
+ name = name.sub(/^[^\.]+\./,'')
109
+ return fact_value.get(name, default)
110
+ else
111
+ # not an object, return default
112
+ return default
113
+ end
114
+ else
115
+ # this is a simple request, return the fact
116
+ return fact_value
117
+ end
118
+ end
119
+ end
120
+
121
+ return default
122
+ end
123
+
124
+ def make_filter(filter)
125
+ filter = filter.collect{|k,v| "#{k}/#{v}" } if filter.is_a?(Hash)
126
+ filter = filter.collect{|i| i.sub(':','/') }.join("/") if filter.is_a?(Array)
127
+ return filter
128
+ end
129
+
130
+ def http_status(code)
131
+ return (HTTP_STATUS_CODES[code.to_i] || nil)
132
+ end
133
+ end
134
+ end
135
+
136
+ class String
137
+ include Onering::Util::String
138
+ end
139
+
140
+ class Module
141
+ def submodules
142
+ constants.collect {|const_name| const_get(const_name)}.select {|const| const.class == Module}
143
+ end
144
+ end
145
+
146
+ class NilClass
147
+ def <=>(*args)
148
+ return 1
149
+ end
150
+ end