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.
- checksums.yaml +15 -0
- data/bin/onering +68 -0
- data/lib/etc/facter.list +19 -0
- data/lib/onering.rb +32 -0
- data/lib/onering/api.rb +322 -0
- data/lib/onering/cli.rb +92 -0
- data/lib/onering/cli/assets.rb +138 -0
- data/lib/onering/cli/automation.rb +62 -0
- data/lib/onering/cli/call.rb +53 -0
- data/lib/onering/cli/devices.rb +98 -0
- data/lib/onering/cli/fact.rb +22 -0
- data/lib/onering/cli/reporter.rb +121 -0
- data/lib/onering/config.rb +62 -0
- data/lib/onering/logger.rb +141 -0
- data/lib/onering/plugins/assets.rb +54 -0
- data/lib/onering/plugins/authentication.rb +35 -0
- data/lib/onering/plugins/automation.rb +70 -0
- data/lib/onering/plugins/reporter.rb +360 -0
- data/lib/onering/util.rb +150 -0
- data/lib/onering/version.rb +8 -0
- metadata +188 -0
@@ -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
|
data/lib/onering/util.rb
ADDED
@@ -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
|