nv-client 1.66.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ module Nv
2
+ module Client
3
+ require 'nventory'
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ module Nv
2
+ module Client
3
+ VERSION = "1.66.0"
4
+ end
5
+ end
@@ -0,0 +1,2086 @@
1
+ begin
2
+ # Try loading facter w/o gems first so that we don't introduce a
3
+ # dependency on gems if it is not needed.
4
+ require 'facter' # Facter
5
+ rescue LoadError
6
+ require 'rubygems'
7
+ require 'facter'
8
+ end
9
+ require 'facter/util/memory' # used for converting MB to GB and stuff
10
+ require 'uri'
11
+ require 'net/http'
12
+ require 'net/https'
13
+ require 'cgi'
14
+ require 'rexml/document'
15
+ require 'yaml'
16
+
17
+ # Only use json gem if its there
18
+ begin
19
+ require 'rubygems'
20
+ require 'json'
21
+ HAS_JSON_GEM = true
22
+ rescue LoadError
23
+ HAS_JSON_GEM = false
24
+ end
25
+
26
+ # fix for ruby http bug where it encodes the params incorrectly
27
+ class Net::HTTP::Put
28
+ def set_form_data(params, sep = '&')
29
+ params_array = params.map do |k,v|
30
+ if v.is_a? Array
31
+ v.inject([]){|c, val| c << "#{urlencode(k.to_s)}=#{urlencode(val.to_s)}"}.join(sep)
32
+ else
33
+ "#{urlencode(k.to_s)}=#{urlencode(v.to_s)}"
34
+ end
35
+ end
36
+ self.body = params_array.join(sep)
37
+ self.content_type = 'application/x-www-form-urlencoded'
38
+ end
39
+ end
40
+
41
+ module PasswordCallback
42
+ @@password = nil
43
+ def self.get_password
44
+ while !@@password
45
+ system "stty -echo"
46
+ print "Password: "
47
+ @@password = $stdin.gets.chomp
48
+ system "stty echo"
49
+ end
50
+ @@password
51
+ end
52
+ end
53
+
54
+ # Module and class names are constants, and thus have to start with a
55
+ # capital letter.
56
+ module NVentory
57
+ end
58
+
59
+ CONFIG_FILES = ['/etc/nventory.conf', "#{ENV['HOME']}/.nventory.conf"]
60
+
61
+ class NVentory::Client
62
+ attr_accessor :delete
63
+
64
+ def initialize(data=nil,*moredata)
65
+ if data || moredata
66
+ parms = legacy_initializeparms(data,moredata)
67
+ # def initialize(debug=false, dryrun=false, configfile=nil, server=nil)
68
+ parms[:debug] ? (@debug = parms[:debug]) : @debug = (nil)
69
+ parms[:dryrun] ? (@dryrun = parms[:dryrun]) : @dryrun = (nil)
70
+ parms[:server] ? (@server = parms[:server]) : @server = (nil)
71
+ parms[:cookiefile] ? @cookiefile = parms[:cookiefile] : @cookiefile = "#{ENV['HOME']}/.nventory_cookie"
72
+ if parms[:proxy_server] == false
73
+ @proxy_server = 'nil'
74
+ elsif parms[:proxy_server]
75
+ @proxy_server = parms[:proxy_server]
76
+ else
77
+ @proxy_server = nil
78
+ end
79
+ parms[:sso_server] ? (@sso_server = parms[:sso_server]) : (@sso_server = nil)
80
+ parms[:configfile] ? (configfile = parms[:configfile]) : (configfile = nil)
81
+ end
82
+ @ca_file = nil
83
+ @ca_path = nil
84
+ @dhparams = '/etc/nventory/dhparams'
85
+ @delete = false # Initialize the variable, see attr_accessor above
86
+ @dmi_data = nil
87
+
88
+ CONFIG_FILES << configfile if configfile
89
+
90
+ CONFIG_FILES.each do |configfile|
91
+ if File.exist?(configfile)
92
+ IO.foreach(configfile) do |line|
93
+ line.chomp!
94
+ next if (line =~ /^\s*$/); # Skip blank lines
95
+ next if (line =~ /^\s*#/); # Skip comments
96
+ key, value = line.split(/\s*=\s*/, 2)
97
+ if key == 'server'
98
+ @server = value
99
+ # Warn the user, as this could potentially be confusing
100
+ # if they don't realize there's a config file lying
101
+ # around
102
+ warn "Using server #{@server} from #{configfile}" if (@debug)
103
+ elsif key == 'sso_server' && !@sso_server
104
+ @sso_server = value
105
+ warn "Using sso_server #{@sso_server} from #{configfile}" if (@debug)
106
+ elsif key == 'proxy_server' && !@proxy_server
107
+ @proxy_server = value
108
+ warn "Using proxy_server #{@proxy_server} from #{configfile}" if (@debug)
109
+ elsif key == 'ca_file'
110
+ @ca_file = value
111
+ warn "Using ca_file #{@ca_file} from #{configfile}" if (@debug)
112
+ elsif key == 'ca_path'
113
+ @ca_path = value
114
+ warn "Using ca_path #{@ca_path} from #{configfile}" if (@debug)
115
+ elsif key == 'dhparams'
116
+ @dhparams = value
117
+ warn "Using dhparams #{@dhparams} from #{configfile}" if (@debug)
118
+ elsif key == 'cookiefile'
119
+ @cookiefile = value
120
+ warn "Using cookiefile #{@cookiefile} from #{configfile}" if (@debug)
121
+ end
122
+ end
123
+ end
124
+ end
125
+
126
+ unless @server
127
+ @server = 'http://nventory/'
128
+ warn "Using server #{@server}" if @debug
129
+ end
130
+ @sso_server = 'https://sso.example.com/' unless @sso_server
131
+
132
+ # Make sure the server URL ends in a / so that we can append paths to it
133
+ # using URI.join
134
+ if @server !~ %r{/$}
135
+ @server << '/'
136
+ end
137
+ end
138
+
139
+ def legacy_initializeparms(data,moredata)
140
+ # if data is string, it is legacy method of supplying initialize params:
141
+ # def initialize(debug=false, dryrun=false, configfile=nil, server=nil)
142
+ newdata = {}
143
+ if data.kind_of?(Hash)
144
+ newdata = data
145
+ elsif data || moredata
146
+ newdata[:debug] = data
147
+ newdata[:dryrun] = moredata[0]
148
+ newdata[:configfile] = moredata[1]
149
+ if moredata[2]
150
+ server = moredata[2] if moredata[2]
151
+ if server =~ /^http/
152
+ (server =~ /\/$/) ? (newdata[:server] = server) : (newdata[:server] = "#{server}/")
153
+ else
154
+ newdata[:server] = "http://#{server}/"
155
+ end
156
+ end
157
+ newdata[:proxy_server] = moredata[3]
158
+ else
159
+ raise 'Syntax Error'
160
+ end
161
+ warn "** Using server #{newdata[:server]} **" if newdata[:server]
162
+ warn "** Using proxy_server #{newdata[:proxy_server]} **" if newdata[:proxy_server]
163
+ return newdata
164
+ end
165
+
166
+ def legacy_getparms(data,moredata)
167
+ # if data is string, it is legacy method of supplying get_objects params:
168
+ # def get_objects(objecttype, get, exactget, regexget, exclude, andget, includes=nil, login=nil, password_callback=PasswordCallback)
169
+ newdata = {}
170
+ if data.kind_of?(String)
171
+ raise 'Syntax Error: Missing :objecttype' unless data.kind_of?(String)
172
+ newdata[:objecttype] = data
173
+ newdata[:get] = moredata[0]
174
+ newdata[:exactget] = moredata[1]
175
+ newdata[:regexget] = moredata[2]
176
+ newdata[:exclude] = moredata[3]
177
+ newdata[:andget] = moredata[4]
178
+ newdata[:includes] = moredata[5]
179
+ newdata[:login] = moredata[6]
180
+ newdata[:password_callback] = PasswordCallback
181
+ elsif data.kind_of?(Hash)
182
+ raise 'Syntax Error: Missing :objecttype' unless data[:objecttype].kind_of?(String)
183
+ newdata = data
184
+ newdata[:password_callback] = PasswordCallback unless newdata[:password_callback]
185
+ else
186
+ raise 'Syntax Error'
187
+ end
188
+ return newdata
189
+ end
190
+
191
+ # FIXME: get, exactget, regexget, exclude and includes should all merge into
192
+ # a single search options hash parameter
193
+ def get_objects(data,*moredata)
194
+ parms = legacy_getparms(data,moredata)
195
+ # def get_objects(objecttype, get, exactget, regexget, exclude, andget, includes=nil, login=nil, password_callback=PasswordCallback)
196
+ objecttype = parms[:objecttype]
197
+ get = parms[:get]
198
+ exactget = parms[:exactget]
199
+ regexget = parms[:regexget]
200
+ exclude = parms[:exclude]
201
+ andget = parms[:andget]
202
+ includes = parms[:includes]
203
+ login = parms[:login]
204
+ password_callback = parms[:password_callback]
205
+ # PS-704 - node_groups controller when format.xml, includes some custom model methods that create a lot of querying joins, so this is
206
+ # a way to 'override' it on cli side - the server will look for that param to skip these def methods when it renders. webparams = {:nodefmeth => 1}
207
+ webparams = parms[:webparams]
208
+ #
209
+ # Package up the search parameters in the format the server expects
210
+ #
211
+ metaget = []
212
+ if get
213
+ get.each_pair do |key,values|
214
+ if key == 'enable_aliases' && values == 1
215
+ metaget << "#{key}=#{values}"
216
+ elsif values.length > 1
217
+ values.each do |value|
218
+ metaget << "#{key}[]=#{CGI.escape(value)}"
219
+ end
220
+ else
221
+ # This isn't strictly necessary, specifying a single value via
222
+ # 'key[]=[value]' would work fine, but this makes for a cleaner URL
223
+ # and slightly reduced processing on the backend
224
+ metaget << "#{key}=#{CGI.escape(values[0])}"
225
+ end
226
+ end
227
+ end
228
+ if exactget
229
+ exactget.each_pair do |key,values|
230
+ if key == 'enable_aliases' && values == 1
231
+ metaget << "#{key}=#{values}"
232
+ elsif values.length > 1
233
+ values.each do |value|
234
+ metaget << "exact_#{key}[]=#{CGI.escape(value)}"
235
+ end
236
+ else
237
+ # This isn't strictly necessary, specifying a single value via
238
+ # 'key[]=[value]' would work fine, but this makes for a cleaner URL
239
+ # and slightly reduced processing on the backend
240
+ metaget << "exact_#{key}=#{CGI.escape(values[0])}"
241
+ end
242
+ end
243
+ end
244
+ if regexget
245
+ regexget.each_pair do |key,values|
246
+ if values.length > 1
247
+ values.each do |value|
248
+ metaget << "regex_#{key}[]=#{CGI.escape(value)}"
249
+ end
250
+ else
251
+ # This isn't strictly necessary, specifying a single value via
252
+ # 'key[]=[value]' would work fine, but this makes for a cleaner URL
253
+ # and slightly reduced processing on the backend
254
+ metaget << "regex_#{key}=#{CGI.escape(values[0])}"
255
+ end
256
+ end
257
+ end
258
+ if exclude
259
+ exclude.each_pair do |key,values|
260
+ if values.length > 1
261
+ values.each do |value|
262
+ metaget << "exclude_#{key}[]=#{CGI.escape(value)}"
263
+ end
264
+ else
265
+ # This isn't strictly necessary, specifying a single value via
266
+ # 'key[]=[value]' would work fine, but this makes for a cleaner URL
267
+ # and slightly reduced processing on the backend
268
+ metaget << "exclude_#{key}=#{CGI.escape(values[0])}"
269
+ end
270
+ end
271
+ end
272
+ if andget
273
+ andget.each_pair do |key,values|
274
+ if values.length > 1
275
+ values.each do |value|
276
+ metaget << "and_#{key}[]=#{CGI.escape(value)}"
277
+ end
278
+ else
279
+ # This isn't strictly necessary, specifying a single value via
280
+ # 'key[]=[value]' would work fine, but this makes for a cleaner URL
281
+ # and slightly reduced processing on the backend
282
+ metaget << "and_#{key}=#{CGI.escape(values[0])}"
283
+ end
284
+ end
285
+ end
286
+ if includes
287
+ # includes = ['status', 'rack:datacenter']
288
+ # maps to
289
+ # include[status]=&include[rack]=datacenter
290
+ includes.each do |inc|
291
+ incstring = ''
292
+ if inc.include?(':')
293
+ incparts = inc.split(':')
294
+ lastpart = incparts.pop
295
+ incstring = 'include'
296
+ incparts.each { |part| incstring << "[#{part}]" }
297
+ incstring << "=#{lastpart}"
298
+ else
299
+ incstring = "include[#{inc}]="
300
+ end
301
+ metaget << incstring
302
+ end
303
+ end
304
+ if webparams && webparams.kind_of?(Hash)
305
+ webparams.each_pair{|k,v| metaget << "#{k}=#{v}"}
306
+ end
307
+
308
+ querystring = metaget.join('&')
309
+
310
+ #
311
+ # Send the query to the server
312
+ #
313
+
314
+ if parms[:format] == 'json'
315
+ if HAS_JSON_GEM
316
+ uri = URI::join(@server, "#{objecttype}.json?#{querystring}")
317
+ else
318
+ warn "Warning: Cannot use json format because json gem is not installed. Using xml format instead."
319
+ parms[:format] = 'xml'
320
+ uri = URI::join(@server, "#{objecttype}.xml?#{querystring}")
321
+ end
322
+ else
323
+ uri = URI::join(@server, "#{objecttype}.xml?#{querystring}")
324
+ end
325
+
326
+ req = Net::HTTP::Get.new(uri.request_uri)
327
+ warn "GET URL: #{uri}" if (@debug)
328
+ response = send_request(req, uri, login, password_callback)
329
+ while response.kind_of?(Net::HTTPMovedPermanently)
330
+ uri = URI.parse(response['Location'])
331
+ req = Net::HTTP::Get.new(uri.request_uri)
332
+ response = send_request(req, uri, login, password_callback)
333
+ end
334
+ if !response.kind_of?(Net::HTTPOK)
335
+ puts response.body
336
+ response.error!
337
+ end
338
+
339
+ if parms[:format] == 'json'
340
+ results = JSON.parse(response.body)
341
+ else
342
+ #
343
+ # Parse the XML data from the server
344
+ # This tries to render the XML into the best possible representation
345
+ # as a Perl hash. It may need to evolve over time.
346
+ puts response.body if (@debug)
347
+ results_xml = REXML::Document.new(response.body)
348
+ results = {}
349
+ if results_xml.root.elements["/#{objecttype}"]
350
+ results_xml.root.elements["/#{objecttype}"].each do |elem|
351
+ # For some reason Elements[] is returning things other than elements,
352
+ # like text nodes
353
+ next if elem.node_type != :element
354
+ data = xml_to_ruby(elem)
355
+ name = data['name'] || data['id']
356
+ if !results[name].nil?
357
+ warn "Duplicate entries for #{name}. Only one will be shown."
358
+ end
359
+ results[name] = data
360
+ end
361
+ end
362
+ end
363
+
364
+ #puts results.inspect if (@debug)
365
+ puts YAML.dump(results) if (@debug)
366
+ results
367
+ end
368
+
369
+ def get_field_names(objecttype, login=nil, password_callback=PasswordCallback)
370
+ uri = URI::join(@server, "#{objecttype}/field_names.xml")
371
+ req = Net::HTTP::Get.new(uri.request_uri)
372
+ warn "GET URL: #{uri}" if (@debug)
373
+ response = send_request(req, uri, login, password_callback)
374
+ while response.kind_of?(Net::HTTPMovedPermanently)
375
+ uri = URI.parse(response['Location'])
376
+ req = Net::HTTP::Get.new(uri.request_uri)
377
+ response = send_request(req, uri, login, password_callback)
378
+ end
379
+ if !response.kind_of?(Net::HTTPOK)
380
+ puts response.body
381
+ response.error!
382
+ end
383
+
384
+ puts response.body if (@debug)
385
+ results_xml = REXML::Document.new(response.body)
386
+ field_names = []
387
+ results_xml.root.elements['/field_names'].each do |elem|
388
+ # For some reason Elements[] is returning things other than elements,
389
+ # like text nodes
390
+ next if elem.node_type != :element
391
+ field_names << elem.text
392
+ end
393
+
394
+ field_names
395
+ end
396
+
397
+ def get_expanded_nodegroup(nodegroup)
398
+ getdata = {}
399
+ getdata[:objecttype] = 'node_groups'
400
+ getdata[:exactget] = {'name' => [nodegroup]}
401
+ getdata[:includes] = ['nodes', 'child_groups']
402
+ results = get_objects(getdata)
403
+ nodes = {}
404
+ if results.has_key?(nodegroup)
405
+ if results[nodegroup].has_key?('nodes')
406
+ results[nodegroup]['nodes'].each { |node| nodes[node['name']] = true }
407
+ end
408
+ if results[nodegroup].has_key?('child_groups')
409
+ results[nodegroup]['child_groups'].each do |child_group|
410
+ get_expanded_nodegroup(child_group['name']).each { |child_group_node| nodes[child_group_node] = true }
411
+ end
412
+ end
413
+ end
414
+ nodes.keys.sort
415
+ end
416
+
417
+ # The results argument can be a reference to a hash returned by a
418
+ # call to get_objects, in which case the data will be PUT to each object
419
+ # there, thus updating them. Or it can be nil, in which case the
420
+ # data will be POSTed to create a new entry.
421
+ def set_objects(objecttypes, results, data, login, password_callback=PasswordCallback)
422
+ # Convert any keys which don't already specify a model
423
+ # from 'foo' to 'objecttype[foo]'
424
+ objecttype = singularize(objecttypes)
425
+ cleandata = {}
426
+ data.each_pair do |key, value|
427
+ if key !~ /\[.+\]/
428
+ cleandata["#{objecttype}[#{key}]"] = value
429
+ else
430
+ cleandata[key] = value
431
+ end
432
+ end
433
+
434
+ #puts cleandata.inspect if (@debug)
435
+ puts YAML.dump(cleandata) if (@debug)
436
+
437
+ successcount = 0
438
+ if results && !results.empty?
439
+ results.each_pair do |result_name, result|
440
+ if @delete
441
+ warn "Deleting objects via set_objects is deprecated, use delete_objects instead"
442
+ uri = URI::join(@server, "#{objecttypes}/#{result['id']}.xml")
443
+ req = Net::HTTP::Delete.new(uri.request_uri)
444
+ req.set_form_data(cleandata)
445
+ response = send_request(req, uri, login, password_callback)
446
+ while response.kind_of?(Net::HTTPMovedPermanently)
447
+ uri = URI.parse(response['Location'])
448
+ req = Net::HTTP::Delete.new(uri.request_uri)
449
+ response = send_request(req, uri, login, password_callback)
450
+ end
451
+ if response.kind_of?(Net::HTTPOK)
452
+ successcount += 1
453
+ else
454
+ puts "DELETE to #{uri} failed for #{result_name}:"
455
+ puts response.body
456
+ end
457
+ # PUT to update an existing object
458
+ elsif result['id']
459
+ uri = URI::join(@server, "#{objecttypes}/#{result['id']}.xml")
460
+ req = Net::HTTP::Put.new(uri.request_uri)
461
+ req.set_form_data(cleandata)
462
+ warn "PUT to URL: #{uri}" if (@debug)
463
+ if !@dryrun
464
+ response = send_request(req, uri, login, password_callback)
465
+ while response.kind_of?(Net::HTTPMovedPermanently)
466
+ uri = URI.parse(response['Location'])
467
+ req = Net::HTTP::Put.new(uri.request_uri)
468
+ req.set_form_data(cleandata)
469
+ response = send_request(req, uri, login, password_callback)
470
+ end
471
+ if response.kind_of?(Net::HTTPOK)
472
+ successcount += 1
473
+ else
474
+ puts "PUT to #{uri} failed for #{result_name}:"
475
+ puts response.body
476
+ end
477
+ end
478
+ else
479
+ warn "set_objects passed a bogus results hash, #{result_name} has no id field"
480
+ end
481
+ end
482
+ else
483
+ uri = URI::join(@server, "#{objecttypes}.xml")
484
+ req = Net::HTTP::Post.new(uri.request_uri)
485
+ req.set_form_data(cleandata)
486
+ warn "POST to URL: #{uri}" if (@debug)
487
+ if !@dryrun
488
+ response = send_request(req, uri, login, password_callback)
489
+ while response.kind_of?(Net::HTTPMovedPermanently)
490
+ uri = URI.parse(response['Location'])
491
+ req = Net::HTTP::Post.new(uri.request_uri)
492
+ req.set_form_data(cleandata)
493
+ response = send_request(req, uri, login, password_callback)
494
+ end
495
+ if response.kind_of?(Net::HTTPOK) || response.kind_of?(Net::HTTPCreated)
496
+ successcount += 1
497
+ else
498
+ puts "POST to #{uri} failed."
499
+ puts response.body
500
+ end
501
+ end
502
+ end
503
+
504
+ successcount
505
+ end
506
+
507
+ # The results argument should be a reference to a hash returned by a
508
+ # call to get_objects.
509
+ def delete_objects(objecttypes, results, login, password_callback=PasswordCallback)
510
+ successcount = 0
511
+ results.each_pair do |result_name, result|
512
+ if result['id']
513
+ uri = URI::join(@server, "#{objecttypes}/#{result['id']}.xml")
514
+ req = Net::HTTP::Delete.new(uri.request_uri)
515
+ response = send_request(req, uri, login, password_callback)
516
+ while response.kind_of?(Net::HTTPMovedPermanently)
517
+ uri = URI.parse(response['Location'])
518
+ req = Net::HTTP::Delete.new(uri.request_uri)
519
+ response = send_request(req, uri, login, password_callback)
520
+ end
521
+ if response.kind_of?(Net::HTTPOK)
522
+ successcount = 0
523
+ else
524
+ warn "Delete of #{result_name} (#{result['id']}) failed:\n" + response.body
525
+ end
526
+ else
527
+ warn "delete_objects passed a bogus results hash, #{result_name} has no id field"
528
+ end
529
+ end
530
+ successcount
531
+ end
532
+
533
+ def register
534
+ data = {}
535
+
536
+ # Tell facter to load everything, otherwise it tries to dynamically
537
+ # load the individual fact libraries using a very broken mechanism
538
+ Facter.loadfacts
539
+
540
+ #
541
+ # Gather software-related information
542
+ #
543
+ data['name'] = Facter['fqdn'].value
544
+ data['updated_at'] = Time.now.strftime("%Y-%m-%d %H:%M:%S")
545
+ if Facter['kernel'] && Facter['kernel'].value == 'Linux' &&
546
+ Facter['lsbdistdescription'] && Facter['lsbdistdescription'].value
547
+ # Strip release version and code name from lsbdistdescription
548
+ lsbdistdesc = Facter['lsbdistdescription'].value
549
+ lsbdistdesc.gsub!(/ release \S+/, '')
550
+ lsbdistdesc.gsub!(/ \([^)]\)/, '')
551
+ data['operating_system[variant]'] = lsbdistdesc
552
+ data['operating_system[version_number]'] = Facter['lsbdistrelease'].value
553
+ elsif Facter['kernel'] && Facter['kernel'].value == 'Darwin' &&
554
+ Facter['macosx_productname'] && Facter['macosx_productname'].value
555
+ data['operating_system[variant]'] = Facter['macosx_productname'].value
556
+ data['operating_system[version_number]'] = Facter['macosx_productversion'].value
557
+ else
558
+ data['operating_system[variant]'] = Facter['operatingsystem'].value
559
+ data['operating_system[version_number]'] = Facter['operatingsystemrelease'].value
560
+ end
561
+ if Facter['architecture'] && Facter['architecture'].value
562
+ data['operating_system[architecture]'] = Facter['architecture'].value
563
+ else
564
+ # Not sure if this is reasonable
565
+ data['operating_system[architecture]'] = Facter['hardwaremodel'].value
566
+ end
567
+ data['kernel_version'] = Facter['kernelrelease'].value
568
+ if Facter.value('memorysize')
569
+ data['os_memory'] = Facter.value('memorysize')
570
+ elsif Facter.value('sp_physical_memory') # Mac OS X
571
+ # More or less a safe bet that OS memory == physical memory on Mac OS X
572
+ data['os_memory'] = Facter.value('sp_physical_memory')
573
+ end
574
+ if Facter['swapsize']
575
+ data['swap'] = Facter['swapsize'].value
576
+ end
577
+ # Currently the processorcount fact doesn't even get defined on most platforms
578
+ if Facter['processorcount'] && Facter['processorcount'].value
579
+ # This is generally a virtual processor count (cores and HTs),
580
+ # not a physical CPU count
581
+ data['os_processor_count'] = Facter['processorcount'].value
582
+ elsif Facter['sp_number_processors'] && Facter['sp_number_processors'].value
583
+ data['os_processor_count'] = Facter['sp_number_processors'].value
584
+ end
585
+ data['timezone'] = Facter['timezone'].value if Facter['timezone']
586
+
587
+ # Need custom facts for these
588
+ #data['virtual_client_ids'] =
589
+
590
+ cpu_percent = getcpupercent
591
+ login_count = getlogincount
592
+ disk_usage = getdiskusage
593
+ # have to round it up because server code only takes integer
594
+ data['utilization_metric[percent_cpu][value]'] = cpu_percent.round if cpu_percent
595
+ data['utilization_metric[login_count][value]'] = login_count if login_count
596
+ data['used_space'] = disk_usage[:used_space] if disk_usage
597
+ data['avail_space'] = disk_usage[:avail_space] if disk_usage
598
+ getvolumes.each do |key, value|
599
+ data[key] = value
600
+ end
601
+
602
+ #
603
+ # Gather hardware-related information
604
+ #
605
+ hardware_profile = NVentory::Client::get_hardware_profile
606
+ data['hardware_profile[manufacturer]'] = hardware_profile[:manufacturer]
607
+ data['hardware_profile[model]'] = hardware_profile[:model]
608
+ if Facter['serialnumber'] && Facter['serialnumber'].value
609
+ data['serial_number'] = Facter['serialnumber'].value
610
+ elsif Facter['sp_serial_number'] && Facter['sp_serial_number'].value # Mac OS X
611
+ data['serial_number'] = Facter['sp_serial_number'].value
612
+ end
613
+ if Facter['processor0'] && Facter['processor0'].value
614
+ # FIXME: Parsing this string is less than ideal, as these things
615
+ # are reported as seperate fields by dmidecode, but facter isn't
616
+ # reading that data.
617
+ # Example: Intel(R) Core(TM)2 Duo CPU T7300 @ 2.00GHz
618
+ # Example: Intel(R) Pentium(R) 4 CPU 3.60GHz
619
+ processor = Facter['processor0'].value
620
+ if processor =~ /(\S+)\s(.+)/
621
+ manufacturer = $1
622
+ model = $2
623
+ speed = nil
624
+ if model =~ /(.+\S)\s+\@\s+([\d\.]+.Hz)/
625
+ model = $1
626
+ speed = $2
627
+ elsif model =~ /(.+\S)\s+([\d\.]+.Hz)/
628
+ model = $1
629
+ speed = $2
630
+ end
631
+ data['processor_manufacturer'] = manufacturer.gsub(/\(R\)/, '')
632
+ data['processor_model'] = model
633
+ data['processor_speed'] = speed
634
+ end
635
+ elsif Facter['sp_cpu_type'] && Facter['sp_cpu_type'].value
636
+ # FIXME: Assuming the manufacturer is the first word is
637
+ # less than ideal
638
+ cpu_type = Facter['sp_cpu_type'].value
639
+ if cpu_type =~ /(\S+)\s(.+)/
640
+ data['processor_manufacturer'] = $1
641
+ data['processor_model'] = $2
642
+ end
643
+ data['processor_speed'] = Facter['sp_current_processor_speed'].value
644
+ # It's not clear if system_profiler is reporting the number
645
+ # of physical CPUs or the number seen by the OS. I'm not
646
+ # sure if there are situations in Mac OS where those two can
647
+ # get out of sync. As such this is identical to what is reported
648
+ # for the os_processor_count above.
649
+ data['processor_count'] = Facter['sp_number_processors'].value
650
+ end
651
+
652
+ if Facter['physicalprocessorcount']
653
+ data['processor_count'] = Facter['physicalprocessorcount'].value
654
+ else
655
+ # need to get from dmidecode
656
+ end
657
+
658
+ data['processor_core_count'] = get_cpu_core_count
659
+ #data['processor_socket_count'] =
660
+ #data['power_supply_count'] =
661
+ #data['physical_memory_sizes'] =
662
+
663
+ physical_memory = get_physical_memory
664
+ data['physical_memory'] = Facter::Memory.scale_number(physical_memory, "MB") if physical_memory
665
+
666
+ nics = []
667
+ if Facter['interfaces'] && Facter['interfaces'].value
668
+ nics = Facter['interfaces'].value.split(',')
669
+ nics.each do |nic|
670
+ data["network_interfaces[#{nic}][name]"] = nic
671
+ data["network_interfaces[#{nic}][hardware_address]"] = Facter["macaddress_#{nic}"].value
672
+ #data["network_interfaces[#{nic}][interface_type]"]
673
+ #data["network_interfaces[#{nic}][physical]"] =
674
+ #data["network_interfaces[#{nic}][up]"] =
675
+ #data["network_interfaces[#{nic}][link]"] =
676
+ #data["network_interfaces[#{nic}][autonegotiate]"] =
677
+ #data["network_interfaces[#{nic}][speed]"] =
678
+ #data["network_interfaces[#{nic}][full_duplex]"] =
679
+ # Facter only captures one address per interface
680
+ data["network_interfaces[#{nic}][ip_addresses][0][address]"] = Facter["ipaddress_#{nic}"].value
681
+ data["network_interfaces[#{nic}][ip_addresses][0][address_type]"] = 'ipv4'
682
+ data["network_interfaces[#{nic}][ip_addresses][0][netmask]"] = Facter["netmask_#{nic}"].value
683
+ #data["network_interfaces[#{nic}][ip_addresses][0][broadcast]"] =
684
+ end
685
+ end
686
+ # get additional nic info that facter doesn't know about
687
+ nic_info = get_nic_info
688
+ nic_info.each do |nic, info|
689
+ next if !nics.include?(nic)
690
+ info.each do |key, value|
691
+ data["network_interfaces[#{nic}][#{key}]"] = value
692
+ end
693
+ end
694
+
695
+ # Mark our NIC data as authoritative so that the server properly
696
+ # updates its records (removing any NICs and IPs we don't specify)
697
+ data['network_interfaces[authoritative]'] = true
698
+
699
+ data['uniqueid'] = NVentory::Client::get_uniqueid
700
+
701
+ # TODO: figure out list of guests if it's a host
702
+ vmstatus = getvmstatus
703
+ if vmstatus == 'xenu'
704
+ data['virtualmode'] = 'guest'
705
+ data['virtualarch'] = 'xen'
706
+ elsif vmstatus == 'xen0'
707
+ data['virtualmode'] = 'host'
708
+ data['virtualarch'] = 'xen'
709
+ elsif vmstatus == 'vmware_server'
710
+ data['virtualmode'] = 'host'
711
+ data['virtualarch'] = 'vmware'
712
+ elsif vmstatus == 'vmware'
713
+ data['virtualmode'] = 'guest'
714
+ data['virtualarch'] = 'vmware'
715
+ elsif vmstatus == 'kvm_host'
716
+ data['virtualmode'] = 'host'
717
+ data['virtualarch'] = 'kvm'
718
+ end
719
+
720
+ if vmstatus == 'kvm_host'
721
+ guests = get_kvm_hostinfo
722
+ guests.each do |vm, vminfo|
723
+ data["vmguest[#{vm}][vmimg_size]"] = vminfo['vmimg_size']
724
+ data["vmguest[#{vm}][vmspace_used]"] = vminfo['vmspace_used']
725
+ end
726
+ end
727
+
728
+ # Looks like this no longer works. virtual_client_ids is not valid
729
+ # field and causes ALL nodes to return....
730
+ # if data['hardware_profile[model]'] == 'VMware Virtual Platform'
731
+ # getdata = {}
732
+ # getdata[:objecttype] = 'nodes'
733
+ # getdata[:exactget] = {'virtual_client_ids' => [data['uniqueid']]}
734
+ # getdata[:login] = 'autoreg'
735
+ # results = get_objects(getdata)
736
+ # if results.length == 1
737
+ # data['virtual_parent_node_id'] = results.values.first['id']
738
+ # elsif results.length > 1
739
+ # warn "Multiple hosts claim this virtual client: #{results.keys.sort.join(',')}"
740
+ # end
741
+ # end
742
+
743
+ # Get console info
744
+ console_type = get_console_type
745
+ if console_type == "Dell DRAC"
746
+ data['console_type'] = "Dell DRAC"
747
+
748
+ drac_info = get_drac_info
749
+
750
+ # Create a NIC for the DRAC and associate it this node
751
+ unless drac_info.empty?
752
+ drac_name = (drac_info[:name] && !drac_info[:name].empty?)? drac_info[:name] : "DRAC"
753
+ data["network_interfaces[#{drac_name}][name]"] = drac_name
754
+ data["network_interfaces[#{drac_name}][hardware_address]"] = drac_info[:mac_address]
755
+ data["network_interfaces[#{drac_name}][ip_addresses][0][address]"] = drac_info[:ip_address]
756
+ data["network_interfaces[#{drac_name}][ip_addresses][0][address_type]"] = "ipv4"
757
+ end
758
+ end
759
+
760
+ # See what chassis/blade enclosure the node is in
761
+ chassis = get_chassis_info
762
+ data["chassis[service_tag]"] = chassis[:service_tag] if !chassis.empty?
763
+ data["chassis[slot_num]"] = chassis[:slot_num] if !chassis.empty?
764
+
765
+ #
766
+ # Report data to server
767
+ #
768
+
769
+ # Check to see if there's an existing entry for this host that matches
770
+ # our unique id. If so we want to update it, even if the hostname
771
+ # doesn't match our current hostname (as it probably indicates this
772
+ # host was renamed).
773
+ results = nil
774
+ if data['uniqueid']
775
+ getdata = {}
776
+ getdata[:objecttype] = 'nodes'
777
+ getdata[:exactget] = {'uniqueid' => [data['uniqueid']]}
778
+ getdata[:login] = 'autoreg'
779
+ results = get_objects(getdata)
780
+ #
781
+ # Check for a match of the reverse uniqueid.
782
+ # Background:
783
+ # Dmidecode versions earlier than 2.10 display
784
+ # the first three fields of the UUID in reverse order
785
+ # due to the use of Big-endian rather than Little-endian
786
+ # byte encoding.
787
+ # Starting with version 2.10, dmidecode uses Little-endian
788
+ # when it finds an SMBIOS >= 2.6. UUID's reported from SMBIOS'
789
+ # earlier than 2.6 are considered "incorrect".
790
+ #
791
+ # After a rebuild/upgrade, rather than creating a new node
792
+ # entry for an existing asset, we'll check for the flipped
793
+ # version of the uniqueid.
794
+ #
795
+ if results.empty? && data['uniqueid'].include?('-')
796
+ reverse_uniqueid = [data['uniqueid'].split('-')[0..2].map { |n| n.split(/(\w\w)/).reverse.join }.join('-'), data['uniqueid'].split('-',4)[3]].join('-')
797
+ getdata[:exactget] = {'uniqueid' => [reverse_uniqueid]}
798
+ results = get_objects(getdata)
799
+ end
800
+ end
801
+
802
+ # If we failed to find an existing entry based on the unique id,
803
+ # fall back to the hostname.
804
+ if results.empty? && data['name']
805
+ getdata = {}
806
+ getdata[:objecttype] = 'nodes'
807
+ getdata[:exactget] = {'name' => [data['name']]}
808
+ getdata[:login] = 'autoreg'
809
+ results = get_objects(getdata)
810
+ end
811
+
812
+ # If we failed to find an existing entry based on the uniqueid and hostname,
813
+ # fall back to using serial number. This may still fail to find an entry,
814
+ # if this is a new host, but that's OK as it will leave %results
815
+ # as undef, which triggers set_nodes to create a new entry on the
816
+ # server.
817
+ if results.empty? && data['serial_number'] &&
818
+ !data['serial_number'].empty? &&
819
+ data['serial_number'] != "Not Specified"
820
+ getdata = {}
821
+ getdata[:objecttype] = 'nodes'
822
+ getdata[:exactget] = {'serial_number' => [data['serial_number']]}
823
+ getdata[:login] = 'autoreg'
824
+ results = get_objects(getdata)
825
+ end
826
+
827
+ setresults = set_objects('nodes', results, data, 'autoreg')
828
+ puts "Command successful" if setresults == 1
829
+ end
830
+
831
+ # Add the given node into the given nodegroup by directly
832
+ # creating the node_group_node_assignment
833
+ # First argument is the id of the node
834
+ # Second argument is the id of the nodegroup
835
+ def add_node_group_node_assignment(node_id, node_group_id, login, password_callback=PasswordCallback)
836
+ setdata = {:node_id => node_id, :node_group_id => node_group_id}
837
+ puts "Adding using the following setdata #{setdata.inspect}"
838
+ set_objects('node_group_node_assignments', nil, setdata, login, password_callback)
839
+ end
840
+
841
+ # The first argument is a hash returned by a 'nodes' call to get_objects
842
+ # The second argument is a hash returned by a 'node_groups'
843
+ # call to get_objects
844
+ # This method does the same thing as the add_nodes_to_nodegroups method. However, it
845
+ # will not be susceptible to the race condition mentioned in add_nodes_to_nodegroups method
846
+ # This is because it directly talks to the node_group_node_assignments controller
847
+ def add_node_group_node_assignments(nodes, nodegroups, login, password_callback=PasswordCallback)
848
+ nodegroups.each do |nodegroup_name, nodegroup|
849
+ nodes.each do |nodename, node|
850
+ add_node_group_node_assignment(node['id'], nodegroup['id'], login, password_callback)
851
+ end
852
+ end
853
+ end
854
+
855
+ # The first argument is a hash returned by a 'nodes' call to get_objects
856
+ # The second argument is a hash returned by a 'node_groups'
857
+ # call to get_objects
858
+ # NOTE: For the node groups you must have requested that the server include 'nodes' in the result
859
+ def add_nodes_to_nodegroups(nodes, nodegroups, login, password_callback=PasswordCallback)
860
+ # The server only supports setting a complete list of members of
861
+ # a node group. So we need to retreive the current list of members
862
+ # for each group, merge in the additional nodes that the user wants
863
+ # added, and pass that off to set_nodegroup_assignments to perform
864
+ # the update.
865
+ # FIXME: This should talk directly to the node_group_node_assignments
866
+ # controller, so that we aren't exposed to the race conditions this
867
+ # method currently suffers from.
868
+ nodegroups.each_pair do |nodegroup_name, nodegroup|
869
+ # Use a hash to merge the current and new members and
870
+ # eliminate duplicates
871
+ merged_nodes = nodes.clone
872
+ nodegroup["nodes"].each do |node|
873
+ name = node['name']
874
+ merged_nodes[name] = node
875
+ end
876
+ set_nodegroup_node_assignments(merged_nodes, {nodegroup_name => nodegroup}, login, password_callback)
877
+ end
878
+ end
879
+ # The first argument is a hash returned by a 'nodes' call to get_objects
880
+ # The second argument is a hash returned by a 'node_groups'
881
+ # call to get_objects
882
+ # NOTE: For the node groups you must have requested that the server include 'nodes' in the result
883
+ def remove_nodes_from_nodegroups(nodes, nodegroups, login, password_callback=PasswordCallback)
884
+ # The server only supports setting a complete list of members of
885
+ # a node group. So we need to retreive the current list of members
886
+ # for each group, remove the nodes that the user wants
887
+ # removed, and pass that off to set_nodegroup_assignments to perform
888
+ # the update.
889
+ # FIXME: This should talk directly to the node_group_node_assignments
890
+ # controller, so that we aren't exposed to the race conditions this
891
+ # method currently suffers from.
892
+ nodegroups.each_pair do |nodegroup_name, nodegroup|
893
+ desired_nodes = {}
894
+
895
+ nodegroup['nodes'].each do |node|
896
+ name = node['name']
897
+ if !nodes.has_key?(name)
898
+ desired_nodes[name] = node
899
+ end
900
+ end
901
+
902
+ set_nodegroup_node_assignments(desired_nodes, {nodegroup_name => nodegroup}, login, password_callback)
903
+ end
904
+ end
905
+ # The first argument is a hash returned by a 'nodes' call to get_objects
906
+ # The second argument is a hash returned by a 'node_groups'
907
+ # call to get_objects
908
+ def set_nodegroup_node_assignments(nodes, nodegroups, login, password_callback=PasswordCallback)
909
+ node_ids = []
910
+ nodes.each_pair do |node_name, node|
911
+ if node['id']
912
+ node_ids << node['id']
913
+ else
914
+ warn "set_nodegroup_node_assignments passed a bogus nodes hash, #{node_name} has no id field"
915
+ end
916
+ end
917
+
918
+ nodegroupdata = {}
919
+ node_ids = 'nil' if node_ids.empty?
920
+ nodegroupdata['node_group_node_assignments[nodes][]'] = node_ids
921
+
922
+ set_objects('node_groups', nodegroups, nodegroupdata, login, password_callback)
923
+ end
924
+
925
+ # Both arguments are hashes returned by a 'node_groups' call to get_objects
926
+ # NOTE: For the parent groups you must have requested that the server include 'child_groups' in the result
927
+ def add_nodegroups_to_nodegroups(child_groups, parent_groups, login, password_callback=PasswordCallback)
928
+ # The server only supports setting a complete list of assignments for
929
+ # a node group. So we need to retreive the current list of assignments
930
+ # for each group, merge in the additional node groups that the user wants
931
+ # added, and pass that off to set_nodegroup_nodegroup_assignments to perform
932
+ # the update.
933
+ # FIXME: This should talk directly to the node_group_node_groups_assignments
934
+ # controller, so that we aren't exposed to the race conditions this
935
+ # method currently suffers from.
936
+ parent_groups.each_pair do |parent_group_name, parent_group|
937
+ # Use a hash to merge the current and new members and
938
+ # eliminate duplicates
939
+ merged_nodegroups = child_groups
940
+
941
+ if parent_group['child_groups']
942
+ parent_group['child_groups'].each do |child_group|
943
+ name = child_group['name']
944
+ merged_nodegroups[name] = child_group
945
+ end
946
+ end
947
+
948
+ set_nodegroup_nodegroup_assignments(merged_nodegroups, {parent_group_name => parent_group}, login, password_callback)
949
+ end
950
+ end
951
+ # Both arguments are hashes returned by a 'node_groups' call to get_objects
952
+ # NOTE: For the parent groups you must have requested that the server include 'child_groups' in the result
953
+ def remove_nodegroups_from_nodegroups(child_groups, parent_groups, login, password_callback=PasswordCallback)
954
+ # The server only supports setting a complete list of assignments for
955
+ # a node group. So we need to retrieve the current list of assignments
956
+ # for each group, remove the node groups that the user wants
957
+ # removed, and pass that off to set_nodegroup_nodegroup_assignments to perform
958
+ # the update.
959
+ # FIXME: This should talk directly to the node_group_node_groups_assignments
960
+ # controller, so that we aren't exposed to the race conditions this
961
+ # method currently suffers from.
962
+ parent_groups.each_pair do |parent_group_name, parent_group|
963
+ desired_child_groups = {}
964
+ if parent_group['child_groups']
965
+ parent_group['child_groups'].each do |child_group|
966
+ name = child_group['name']
967
+ if !child_groups.has_key?(name)
968
+ desired_child_groups[name] = child_group
969
+ end
970
+ end
971
+ end
972
+
973
+ set_nodegroup_nodegroup_assignments(desired_child_groups, {parent_group_name => parent_group}, login, password_callback)
974
+ end
975
+ end
976
+
977
+ # Both arguments are hashes returned by a 'node_groups' call to get_objects
978
+ def set_nodegroup_nodegroup_assignments(child_groups, parent_groups, login, password_callback=PasswordCallback)
979
+ child_ids = []
980
+ child_groups.each_pair do |child_group_name, child_group|
981
+ if child_group['id']
982
+ child_ids << child_group['id']
983
+ else
984
+ warn "set_nodegroup_nodegroup_assignments passed a bogus child groups hash, #{child_group_name} has no id field"
985
+ end
986
+ end
987
+ # cannot pass empty hash therefore, add a 'nil' string. nasty hack and accomodated on the server side code
988
+ child_ids << 'nil' if child_ids.empty?
989
+ nodegroupdata = {}
990
+ nodegroupdata['node_group_node_group_assignments[child_groups][]'] = child_ids
991
+ set_objects('node_groups', parent_groups, nodegroupdata, login, password_callback)
992
+ end
993
+
994
+ # Add a new or pre-existing tag (by name string) to a node_group (by hash returned from get_objects)
995
+ def add_tag_to_node_group(ng_hash, tag_name, login, password_callback=PasswordCallback)
996
+ tag_found = get_objects({:objecttype => 'tags', :exactget => {:name => tag_name}})
997
+ if tag_found.empty?
998
+ tagset_data = { :name => tag_name }
999
+ set_objects('tags',{},tagset_data,login, password_callback)
1000
+ tag_found = get_objects({:objecttype => 'tags', :exactget => {:name => tag_name}})
1001
+ end
1002
+ # tag_found is hash, even tho only one result
1003
+ (tag_data = tag_found[tag_found.keys.first]) && (tag_id = tag_data['id'])
1004
+ ng_hash.each_pair do |ng_name,ng_data|
1005
+ setdata = { :taggable_type => 'NodeGroup', :taggable_id => ng_data['id'], :tag_id => tag_id }
1006
+ set_objects('taggings',{},setdata,login,password_callback)
1007
+ end
1008
+ end
1009
+
1010
+ # Add a new or pre-existing tag (by name string) to a node_group (by hash returned from get_objects)
1011
+ def remove_tag_from_node_group(ng_hash, tag_name, login, password_callback=PasswordCallback)
1012
+ tag_found = get_objects({:objecttype => 'tags', :exactget => {:name => tag_name}})
1013
+ if tag_found.empty?
1014
+ puts "ERROR: Could not find any tags with the name #{tag_name}"
1015
+ exit
1016
+ end
1017
+ # tag_found is hash, even tho only one result
1018
+ (tag_data = tag_found[tag_found.keys.first]) && (tag_id = tag_data['id'])
1019
+ taggings_to_del = {}
1020
+ ng_hash.each_pair do |ng_name,ng_data|
1021
+ get_data = {:objecttype => 'taggings',
1022
+ :exactget => { :taggable_type => 'NodeGroup', :taggable_id => ng_data['id'], :tag_id => tag_id } }
1023
+ tagging_found = get_objects(get_data)
1024
+ unless tagging_found.empty?
1025
+ taggings_to_del.merge!(tagging_found)
1026
+ end
1027
+ end
1028
+ if taggings_to_del.empty?
1029
+ puts "ERROR: Could not find any tags \"#{tag_name}\" assigned to those node_groups"
1030
+ else
1031
+ delete_objects('taggings', taggings_to_del, login, password_callback=PasswordCallback)
1032
+ end
1033
+ end
1034
+
1035
+ # Add a new graffiti to given objects. We're assuming that graffiti is a string
1036
+ # of "key:value" format
1037
+ # obj_type is a string that describe the type of the obj (e.g NodeGroup)
1038
+ # obj_hash is the hash returned from calling get_objects
1039
+ def add_graffiti(obj_type, obj_hash, graffiti, login, password_callback=PasswordCallback)
1040
+ name,value = graffiti.split(':')
1041
+ obj_hash.each_value do |obj|
1042
+ set_objects('graffitis', nil,
1043
+ { :name => name,
1044
+ :value => value,
1045
+ :graffitiable_id => obj['id'],
1046
+ :graffitiable_type => obj_type,
1047
+ },
1048
+ login, password_callback);
1049
+ end
1050
+ end
1051
+
1052
+ # Delete the graffiti (based on the name) from the given objects
1053
+ # obj_type is a string that describe the type of the obj (e.g NodeGroup)
1054
+ # obj_hash is the hash returned from calling get_objects
1055
+ def delete_graffiti(obj_type, obj_hash, graffiti_name, login, password_callback=PasswordCallback)
1056
+ obj_hash.each_value do |obj|
1057
+ getdata = {:objecttype => 'graffitis',
1058
+ :exactget => {:name => graffiti_name,
1059
+ :graffitiable_id => obj['id'],
1060
+ :graffitiable_type => obj_type}
1061
+ }
1062
+ graffitis_to_delete = get_objects(getdata)
1063
+ delete_objects('graffitis', graffitis_to_delete, login, password_callback)
1064
+ end
1065
+ end
1066
+
1067
+ def get_service_tree(service_name)
1068
+ getdata = {}
1069
+ getdata[:objecttype] = 'services'
1070
+ getdata[:exactget] = {'name' => [service_name]}
1071
+ getdata[:includes] = ['nodes', 'parent_services']
1072
+ services = {service_name => []}
1073
+ results = get_objects(getdata)
1074
+
1075
+ if results.has_key?(service_name)
1076
+ if results[service_name].has_key?('parent_services') && !results[service_name]['parent_services'].empty?
1077
+ results[service_name]['parent_services'].each do |service|
1078
+ services[service_name] << get_service_tree(service['name'])
1079
+ end
1080
+ else
1081
+ return service_name
1082
+ end
1083
+ else # no such service
1084
+ return {}
1085
+ end
1086
+ return services
1087
+ end
1088
+
1089
+ #
1090
+ # Helper methods
1091
+ #
1092
+ def self.get_uniqueid
1093
+ os = Facter['kernel'].value
1094
+ hardware_profile = NVentory::Client::get_hardware_profile
1095
+ if os == 'Linux' or os == 'FreeBSD'
1096
+ #
1097
+ if File.exist?('/proc/modules') && `grep -q ^xen /proc/modules` && $? == 0
1098
+ uuid = Facter['macaddress'].value
1099
+ # Dell C6100 don't have unique uuid
1100
+ elsif hardware_profile[:manufacturer] =~ /Dell/ && hardware_profile[:model] == 'C6100'
1101
+ uuid = Facter['macaddress'].value
1102
+ else
1103
+ # best to use UUID from dmidecode
1104
+ uuid = getuuid
1105
+ end
1106
+ # Stupid SeaMicro boxes all have the same UUID below. So we won't
1107
+ # want to use it, use mac address instead
1108
+ if uuid && uuid != "78563412-3412-7856-90AB-CDDEEFAABBCC"
1109
+ uniqueid = uuid
1110
+ # next best thing to use is macaddress
1111
+ else
1112
+ uniqueid = Facter['macaddress'].value
1113
+ end
1114
+ elsif Facter['uniqueid'] && Facter['uniqueid'].value
1115
+ # This sucks, it's just using hostid, which is generally tied to an
1116
+ # IP address, not the physical hardware
1117
+ uniqueid = Facter['uniqueid'].value
1118
+ elsif Facter['sp_serial_number'] && Facter['sp_serial_number'].value
1119
+ # I imagine Mac serial numbers are unique
1120
+ uniqueid = Facter['sp_serial_number'].value
1121
+ end
1122
+ return uniqueid
1123
+ end
1124
+
1125
+ def self.getuuid
1126
+ uuid = nil
1127
+ # dmidecode will fail if not run as root
1128
+ if Process.euid != 0
1129
+ raise "This must be run as root"
1130
+ end
1131
+ uuid_entry = `/usr/sbin/dmidecode | grep UUID`
1132
+ if uuid_entry
1133
+ uuid = uuid_entry.split(":")[1]
1134
+ end
1135
+ if uuid
1136
+ return uuid.strip
1137
+ else
1138
+ return nil
1139
+ end
1140
+ end
1141
+
1142
+ def self.get_hardware_profile
1143
+ result = {:manufacturer => 'Unknown', :model => 'Unknown'}
1144
+ if Facter['manufacturer'] && Facter['manufacturer'].value # dmidecode
1145
+ result[:manufacturer] = Facter['manufacturer'].value.strip
1146
+ result[:model] = Facter['productname'].value.strip
1147
+ elsif Facter['sp_machine_name'] && Facter['sp_machine_name'].value # Mac OS X
1148
+ # There's a small chance of this not being true...
1149
+ result[:manufacturer] = 'Apple'
1150
+ result[:model] = Facter['sp_machine_name'].value.strip
1151
+ end
1152
+ return result
1153
+ end
1154
+
1155
+ #
1156
+ # Private methods
1157
+ #
1158
+ private
1159
+
1160
+ def make_http(uri)
1161
+ http = nil
1162
+ if @proxy_server
1163
+ proxyuri = URI.parse(@proxy_server)
1164
+ proxy = Net::HTTP::Proxy(proxyuri.host, proxyuri.port)
1165
+ http = proxy.new(uri.host, uri.port)
1166
+ else
1167
+ http = Net::HTTP.new(uri.host, uri.port)
1168
+ end
1169
+ if uri.scheme == "https"
1170
+ # Eliminate the OpenSSL "using default DH parameters" warning
1171
+ if File.exist?(@dhparams)
1172
+ dh = OpenSSL::PKey::DH.new(IO.read(@dhparams))
1173
+ Net::HTTP.ssl_context_accessor(:tmp_dh_callback)
1174
+ http.tmp_dh_callback = proc { dh }
1175
+ end
1176
+ http.use_ssl = true
1177
+ if @ca_file && File.exist?(@ca_file)
1178
+ http.ca_file = @ca_file
1179
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
1180
+ end
1181
+ if @ca_path && File.directory?(@ca_path)
1182
+ http.ca_path = @ca_path
1183
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
1184
+ end
1185
+ end
1186
+ http
1187
+ end
1188
+
1189
+ # Returns the path to the cookiefile to be used.
1190
+ # Create the file and correct permissions on
1191
+ # the cookiefile if needed
1192
+ def get_cookiefile(login=nil)
1193
+ # autoreg has a special file
1194
+ if login == 'autoreg'
1195
+ @cookiefile = '/root/.nventory_cookie_autoreg'
1196
+ if ! File.directory?('/root')
1197
+ Dir.mkdir('/root')
1198
+ end
1199
+ end
1200
+ # Create the cookie file if it doesn't already exist
1201
+ if !File.exist?(@cookiefile)
1202
+ warn "Creating #{@cookiefile}"
1203
+ File.open(@cookiefile, 'w') { |file| }
1204
+ end
1205
+ # Ensure the permissions on the cookie file are appropriate,
1206
+ # as it will contain a session key that could be used by others
1207
+ # to impersonate this user to the server.
1208
+ st = File.stat(@cookiefile)
1209
+ if st.mode & 07177 != 0
1210
+ warn "Correcting permissions on #{@cookiefile}"
1211
+ File.chmod(st.mode & 0600, @cookiefile)
1212
+ end
1213
+ @cookiefile
1214
+ end
1215
+
1216
+ # Sigh, Ruby doesn't have a library for handling a persistent
1217
+ # cookie store so we have to do the dirty work ourselves. This
1218
+ # is by no means a full implementation, it's just enough to do
1219
+ # what's needed here.
1220
+
1221
+ # Break's a Set-Cookie line up into its constituent parts
1222
+ # Example from http://en.wikipedia.org/wiki/HTTP_cookie:
1223
+ # Set-Cookie: RMID=732423sdfs73242; expires=Fri, 31-Dec-2010 23:59:59 GMT; path=/; domain=.example.net
1224
+ def parse_cookie(line)
1225
+ cookie = nil
1226
+ if line =~ /^Set-Cookie\d?: .+=.+/
1227
+ cookie = {}
1228
+ line.chomp!
1229
+ cookie[:line] = line
1230
+ # Remove the Set-Cookie portion of the line
1231
+ setcookie, rest = line.split(' ', 2)
1232
+ # Then break off the name and value from the cookie attributes
1233
+ namevalue, rawattributes = rest.split('; ', 2)
1234
+ name, value = namevalue.split('=', 2)
1235
+ cookie[:name] = name
1236
+ cookie[:value] = value
1237
+ attributes = {}
1238
+ rawattributes.split('; ').each do |attribute|
1239
+ attrname, attrvalue = attribute.split('=', 2)
1240
+ # The Perl cookie jar uses a non-standard syntax, which seems to
1241
+ # include wrapping some fields (particularly path) in quotes. The
1242
+ # Perl nVentory library uses the Perl cookie jar code so we need to be
1243
+ # compatible with it.
1244
+ if attrvalue =~ /^".*"$/
1245
+ attrvalue.sub!(/^"/, '')
1246
+ attrvalue.sub!(/"$/, '')
1247
+ end
1248
+ # rfc2965, 3.2.2:
1249
+ # If an attribute appears more than once in a cookie, the client
1250
+ # SHALL use only the value associated with the first appearance of
1251
+ # the attribute; a client MUST ignore values after the first.
1252
+ if !attributes[attrname]
1253
+ attributes[attrname] = attrvalue
1254
+ end
1255
+ end
1256
+ cookie[:attributes] = attributes
1257
+ else
1258
+ # Invalid lines in the form of comments and blank lines are to be
1259
+ # expected when we're called by read_cookiefile, so don't treat this as
1260
+ # a big deal.
1261
+ puts "parse_cookie passed invalid line: #{line}" if (@debug)
1262
+ end
1263
+ cookie
1264
+ end
1265
+
1266
+ # Returns an array of cookies from the specified cookiefile
1267
+ def read_cookiefile(cookiefile)
1268
+ warn "Using cookies from #{cookiefile}" if (@debug)
1269
+ cookies = []
1270
+ IO.foreach(cookiefile) do |line|
1271
+ cookie = parse_cookie(line)
1272
+ if cookie && cookie[:attributes] && cookie[:attributes]["expires"]
1273
+ if DateTime.parse(cookie[:attributes]["expires"]) < DateTime.now
1274
+ warn "Cookie expired: #{cookie[:line]}" if @debug
1275
+ next
1276
+ end
1277
+ end
1278
+ if cookie
1279
+ cookies << cookie
1280
+ end
1281
+ end
1282
+ cookies
1283
+ end
1284
+
1285
+ # This returns any cookies in the cookiefile which have domain and path
1286
+ # settings that match the specified uri.
1287
+ def get_cookies_for_uri(cookiefile, uri)
1288
+ cookies = []
1289
+ latest_cookie = []
1290
+ counter = 0
1291
+ read_cookiefile(cookiefile).each do |cookie|
1292
+ next unless uri.host =~ Regexp.new("#{cookie[:attributes]['domain']}$")
1293
+ next unless uri.path =~ Regexp.new("^#{cookie[:attributes]['path'].gsub(/,.*/,'')}") # gsub in case didn't parse out comma seperator for new cookie
1294
+ # if there are more than 1 cookie , we only want the one w/ latest expiration
1295
+ if cookie[:attributes]["expires"]
1296
+ unless latest_cookie.empty?
1297
+ cookie_expiration = DateTime.parse(cookie[:attributes]["expires"])
1298
+ latest_cookie[0] < cookie_expiration ? (latest_cookie = [cookie_expiration, cookie]) : next
1299
+ else
1300
+ latest_cookie = [ DateTime.parse(cookie[:attributes]["expires"]), cookie ]
1301
+ end
1302
+ else
1303
+ cookies << cookie
1304
+ end
1305
+ end
1306
+ cookies << latest_cookie[1] unless latest_cookie.empty?
1307
+ cookies
1308
+ end
1309
+
1310
+ # Extract cookie from response and save it to the user's cookie store
1311
+ def extract_cookie(response, uri, login=nil)
1312
+ if response['set-cookie']
1313
+ cookiefile = get_cookiefile(login)
1314
+ # It doesn't look like it matters for our purposes at the moment, but
1315
+ # according to rfc2965, 3.2.2 the Set-Cookie header can contain more
1316
+ # than one cookie, separated by commas.
1317
+ puts "extract_cookie processing #{response['set-cookie']}" if (@debug)
1318
+ newcookie = parse_cookie('Set-Cookie: ' + response['set-cookie'])
1319
+ return if newcookie.nil?
1320
+
1321
+ # Some cookie fields are optional, and should default to the
1322
+ # values in the request. We need to insert these so that we
1323
+ # save them properly.
1324
+ # http://cgi.netscape.com/newsref/std/cookie_spec.html
1325
+ if !newcookie[:attributes]['domain']
1326
+ puts "Adding domain #{uri.host} to cookie" if (@debug)
1327
+ newcookie = parse_cookie(newcookie[:line] + "; domain=#{uri.host}")
1328
+ end
1329
+ if !newcookie[:attributes]['path']
1330
+ puts "Adding path #{uri.path} to cookie" if (@debug)
1331
+ newcookie = parse_cookie(newcookie[:line] + "; path=#{uri.path}")
1332
+ end
1333
+ cookies = []
1334
+ change = false
1335
+ existing_cookie = false
1336
+ read_cookiefile(cookiefile).each do |cookie|
1337
+ # Remove any existing cookies with the same name, domain and path
1338
+ puts "Comparing #{cookie.inspect} to #{newcookie.inspect}" if (@debug)
1339
+ if cookie[:name] == newcookie[:name] &&
1340
+ cookie[:attributes]['domain'] == newcookie[:attributes]['domain'] &&
1341
+ cookie[:attributes]['path'] == newcookie[:attributes]['path']
1342
+ existing_cookie = true
1343
+ if cookie == newcookie
1344
+ puts "Existing cookie is identical to new cookie" if (@debug)
1345
+ else
1346
+ # Cookie removed by virtue of us not saving it here
1347
+ puts "Replacing existing but not identical cookie #{cookie.inspect}" if (@debug)
1348
+ cookies << newcookie
1349
+ change = true
1350
+ end
1351
+ else
1352
+ puts "Keeping non-matching cookie #{cookie.inspect}" if (@debug)
1353
+ cookies << cookie
1354
+ end
1355
+ end
1356
+ if !existing_cookie
1357
+ puts "No existing cookie matching new cookie, adding new cookie" if (@debug)
1358
+ cookies << newcookie
1359
+ change = true
1360
+ end
1361
+ if change
1362
+ puts "Updating cookiefile #{cookiefile}" if (@debug)
1363
+ File.open(cookiefile, 'w') { |file| file.puts(cookies.collect{|cookie| cookie[:line]}.join("\n")) }
1364
+ else
1365
+ puts "No cookie changes, leaving cookiefile untouched" if (@debug)
1366
+ end
1367
+ else
1368
+ puts "extract_cookie finds no cookie in response" if (@debug)
1369
+ end
1370
+ end
1371
+
1372
+ # Sends requests to the nVentory server and handles any redirects to
1373
+ # authentication pages or services.
1374
+ def send_request(req, uri, login, password_callback=PasswordCallback,loopcounter=0,stopflag=false)
1375
+ if loopcounter > 7
1376
+ if stopflag
1377
+ raise "Infinite loop detected"
1378
+ else
1379
+ warn "Loop detected. Clearing out cookiefile.."
1380
+ loopcounter = 0
1381
+ stopflag = true
1382
+ File.open(get_cookiefile(login), 'w') { |file| file.write(nil) }
1383
+ end
1384
+ end
1385
+ cookies = get_cookies_for_uri(get_cookiefile(login), uri)
1386
+ if !cookies.empty?
1387
+ cookiestring = cookies.collect{|cookie| "#{cookie[:name]}=#{cookie[:value]}" }.join('; ')
1388
+ puts "Inserting cookies into request: #{cookiestring}" if (@debug)
1389
+ req['Cookie'] = cookiestring
1390
+ end
1391
+
1392
+ response = make_http(uri).request(req)
1393
+ extract_cookie(response, uri, login)
1394
+
1395
+ # Check for signs that the server wants us to authenticate
1396
+ password = nil
1397
+ if login == 'autoreg'
1398
+ password = 'mypassword'
1399
+ end
1400
+ # nVentory will redirect to the login controller if authentication is
1401
+ # required. The scheme and port in the redirect location could be either
1402
+ # the standard server or the https variant, depending on whether or not
1403
+ # the server administration has turned on the ssl_requirement plugin.
1404
+ if response.kind_of?(Net::HTTPFound) &&
1405
+ response['Location'] &&
1406
+ URI.parse(response['Location']).host == URI.parse(@server).host &&
1407
+ URI.parse(response['Location']).path == URI.join(@server, 'login/login').path
1408
+ puts "Server responsed with redirect to nVentory login: #{response['Location']}" if (@debug)
1409
+ loginuri = URI.parse(response['Location'])
1410
+ ####################### Fix by darrendao - force it to use https ##########################
1411
+ # This is needed because if you're not usign https, then you will get
1412
+ # redirected to https login page, rather than being logged in. So the check down there will
1413
+ # will.
1414
+ loginuri.scheme = 'https'
1415
+ loginuri = URI.parse(loginuri.to_s)
1416
+ ############################################################################################
1417
+ loginreq = Net::HTTP::Post.new(loginuri.request_uri)
1418
+ if password_callback.kind_of?(Module)
1419
+ password = password_callback.get_password if (!password)
1420
+ else
1421
+ password = password_callback if !password
1422
+ end
1423
+ loginreq.set_form_data({'login' => login, 'password' => password})
1424
+ # Include the cookies so the server doesn't have to generate another
1425
+ # session for us.
1426
+ loginreq['Cookie'] = cookiestring
1427
+ loginresponse = make_http(loginuri).request(loginreq)
1428
+ if @debug
1429
+ puts "nVentory auth POST response (#{loginresponse.code}):"
1430
+ if loginresponse.body.strip.empty?
1431
+ puts '<Body empty>'
1432
+ else
1433
+ puts loginresponse.body
1434
+ end
1435
+ end
1436
+ # The server always sends back a 302 redirect in response to a login
1437
+ # attempt. You get redirected back to the login page if your login
1438
+ # failed, or redirected to your original page or the main page if the
1439
+ # login succeeded.
1440
+ if loginresponse.kind_of?(Net::HTTPFound) &&
1441
+ URI.parse(loginresponse['Location']).path != loginuri.path
1442
+ puts "Authentication against nVentory server succeeded" if (@debug)
1443
+ extract_cookie(loginresponse, loginuri, login)
1444
+ puts "Resending original request now that we've authenticated" if (@debug)
1445
+ return send_request(req, uri, login, password_callback)
1446
+ else
1447
+ puts "Authentication against nVentory server failed" if (@debug)
1448
+ end
1449
+ end
1450
+
1451
+ # An SSO-enabled app will redirect to SSO if authentication is required
1452
+ if response.kind_of?(Net::HTTPFound) && response['Location'] && URI.parse(response['Location']).host == URI.parse(@sso_server).host
1453
+ puts "Server responsed with redirect to SSO login: #{response['Location']}" if (@debug)
1454
+ if login == 'autoreg'
1455
+ loginuri = URI.join(@server, 'login/login')
1456
+ puts "** Login user is 'autoreg'. Changing loginuri to #{loginuri.to_s}" if @debug
1457
+ unless loginuri.scheme == 'https'
1458
+ loginuri.scheme = 'https'
1459
+ loginuri = URI.parse(loginuri.to_s)
1460
+ end
1461
+ else
1462
+ loginuri = URI.parse(response['Location'])
1463
+ end
1464
+ # update the loginuri to the non-redirect uri of sso
1465
+ loginuri.path = '/login'
1466
+ loginuri.query = 'noredirects=1'
1467
+ loginreq = Net::HTTP::Post.new(loginuri.request_uri)
1468
+ if password_callback.kind_of?(Module)
1469
+ password = password_callback.get_password if (!password)
1470
+ else
1471
+ password = password_callback if !password
1472
+ end
1473
+ loginreq.set_form_data({'login' => login, 'password' => password})
1474
+ # It probably doesn't matter, but include the cookies again for good
1475
+ # measure
1476
+ loginreq['Cookie'] = cookiestring
1477
+ # Telling the SSO server we want XML back gets responses that are easier
1478
+ # to parse.
1479
+ loginreq['Accept'] = 'application/xml'
1480
+ loginresponse = make_http(loginuri).request(loginreq)
1481
+ # if it's a redirect (such as due to NON-fqdn) loop so that it follows until no further redirect
1482
+ while [Net::HTTPMovedPermanently, Net::HTTPFound].include?(loginresponse.class)
1483
+ if loginresponse.kind_of?(Net::HTTPFound) && loginresponse['Location'] =~ /sso.*\/session\/token.*/
1484
+ puts "** Found session token" if @debug
1485
+ break
1486
+ end
1487
+ puts "** Following redirect #{loginresponse.class.to_s} => #{loginresponse['Location'].to_s}" if @debug
1488
+ loginuri = URI.parse(loginresponse['Location'])
1489
+ loginreq = Net::HTTP::Post.new(loginuri.request_uri)
1490
+ loginreq.set_form_data({'login' => login, 'password' => password})
1491
+ loginresponse = make_http(loginuri).request(loginreq)
1492
+ end # while loginresponse.kind_of?(Net::HTTPMovedPermanently)
1493
+
1494
+ if @debug
1495
+ puts "AUTH POST response (#{loginresponse.code}):"
1496
+ if loginresponse.body.strip.empty?
1497
+ puts '<Body empty>'
1498
+ else
1499
+ puts loginresponse.body
1500
+ end
1501
+ end
1502
+
1503
+ # SSO does a number of redirects until you get to the right domain but should just follow once and get the cookie, will become Net::HTTPNotAcceptable (406).
1504
+ if loginresponse.kind_of?(Net::HTTPFound) && loginresponse['Location'] =~ /sso.*\/session\/token.*/
1505
+ puts "** Following redirect #{loginresponse.class.to_s} => #{loginresponse['Location'].to_s}" if @debug
1506
+ loginuri = URI.parse(loginresponse['Location'])
1507
+ loginreq = Net::HTTP::Get.new(loginuri.request_uri)
1508
+ loginresponse = make_http(loginuri).request(loginreq)
1509
+ end
1510
+
1511
+ # The SSO server sends back 200 if authentication succeeds, 401 or 403
1512
+ # if it does not.
1513
+ if loginresponse.kind_of?(Net::HTTPSuccess) || (loginresponse.kind_of?(Net::HTTPFound) && loginresponse['Location'] =~ /^#{loginuri.scheme}:\/\/#{loginuri.host}\/$/ ) || loginresponse.kind_of?(Net::HTTPNotAcceptable)
1514
+ puts "Authentication against server succeeded" if (@debug)
1515
+ extract_cookie(loginresponse, loginuri, login)
1516
+ puts "Resending original request now that we've authenticated" if (@debug)
1517
+ loopcounter += 1
1518
+ return send_request(req, uri, login, password_callback, loopcounter,stopflag)
1519
+ else
1520
+ puts "Authentication against server failed" if (@debug)
1521
+ end
1522
+ end
1523
+
1524
+ response
1525
+ end
1526
+
1527
+ def xml_to_ruby(xmlnode)
1528
+ # The server includes a hint as to the type of data structure
1529
+ # in the XML
1530
+ data = nil
1531
+ if xmlnode.attributes['type'] == 'array'
1532
+ data = []
1533
+ xmlnode.elements.each { |child| data << xml_to_ruby(child) }
1534
+ elsif xmlnode.size <= 1
1535
+ data = xmlnode.text
1536
+ else
1537
+ data = {}
1538
+ xmlnode.elements.each do |child|
1539
+ field = child.name
1540
+ data[field] = xml_to_ruby(child)
1541
+ end
1542
+ end
1543
+ data
1544
+ end
1545
+
1546
+ # FIXME: Would be really nice to figure out a way to use the Rails inflector
1547
+ def singularize(word)
1548
+ singular = nil
1549
+ # statuses -> status
1550
+ # ip_addresses -> ip_address
1551
+ if (word =~ /(.*s)es$/)
1552
+ singular = $1
1553
+ # nodes -> node
1554
+ # vips -> vip
1555
+ elsif (word =~ /(.*)s$/)
1556
+ singular = $1
1557
+ else
1558
+ singular = word
1559
+ end
1560
+ singular
1561
+ end
1562
+
1563
+ def find_sar
1564
+ path_env = (ENV['PATH'] || "").split(':')
1565
+ other_paths = ["/usr/bin", "/data/svc/sysstat/bin"]
1566
+ sarname = 'sar'
1567
+ (path_env | other_paths).each do |path|
1568
+ if File.executable?(File.join(path, sarname))
1569
+ return File.join(path, sarname)
1570
+ end
1571
+ end
1572
+ end
1573
+
1574
+ def get_sar_data(sar_dir=nil, day = nil)
1575
+ sar = find_sar
1576
+ result = []
1577
+ cmd = nil
1578
+ ENV['LC_TIME']='POSIX'
1579
+ if day
1580
+ cmd = "#{sar} -u -f #{sar_dir}/sa#{day}"
1581
+ else
1582
+ cmd = "#{sar} -u"
1583
+ end
1584
+ output = `#{cmd}`
1585
+ output.split("\n").each do |line|
1586
+ result << line unless line =~ /(average|cpu|%|linux)/i
1587
+ end
1588
+ result
1589
+ end
1590
+
1591
+ # I'm sure there's a better way to do all of these. However,
1592
+ # I'm just following the way the code was written in Perl.
1593
+ def getcpupercent
1594
+ return nil if !Facter['kernel'] or Facter['kernel'].value != 'Linux'
1595
+ sar_dir = "/var/log/sa"
1596
+ end_time = Time.now
1597
+ start_time = end_time - 60*60*3
1598
+ end_date = end_time.strftime("%d")
1599
+ start_date = start_time.strftime("%d")
1600
+
1601
+ data_points = []
1602
+ # all hours in same day so just make list of all hours to look for
1603
+ if end_date == start_date
1604
+ today_sar = get_sar_data
1605
+ return false if today_sar.empty?
1606
+
1607
+ # We only take avg of last 3 hours
1608
+ (start_time.hour..end_time.hour).each do | hour |
1609
+ hour = "0#{hour}" if hour < 10
1610
+ today_sar.each do |line|
1611
+ data_points << $1.to_f if line =~ /^#{hour}:.*\s(\S+)$/
1612
+ end
1613
+ end
1614
+ else
1615
+ today_sar = get_sar_data
1616
+ yesterday_sar = get_sar_data(sar_dir, start_date)
1617
+ return false if today_sar.empty? or yesterday_sar.empty?
1618
+ # Parse today sar data
1619
+ (0..end_time.hour).each do | hour |
1620
+ hour = "0#{hour}" if hour < 10
1621
+ today_sar.each do |line|
1622
+ data_points << $1.to_f if line =~ /^#{hour}:.*\s(\S+)$/
1623
+ end
1624
+ end
1625
+
1626
+ # Parse yesterday sar data
1627
+ (start_time.hour..23).each do | hour |
1628
+ hour = "0#{hour}" if hour < 10
1629
+ yesterday_sar.each do |line|
1630
+ data_points << $1.to_f if line =~ /^#{hour}:.*\s(\S+)$/
1631
+ end
1632
+ end
1633
+ end
1634
+
1635
+ # no data points
1636
+ return nil if data_points.empty?
1637
+
1638
+ avg = data_points.inject(0.0) { |sum, el| sum + el } / data_points.size
1639
+ # sar reports % idle, so need the opposite
1640
+ result = 100 - avg
1641
+ return result
1642
+ end
1643
+
1644
+ # This is based on the perl version in OSInfo.pm
1645
+ def getlogincount
1646
+
1647
+ # darrendao: Looks like this number has to match up with how often
1648
+ # nventory-client is run in the crontab, otherwise, nventory server ends up
1649
+ # miscalculating the sum... bad...
1650
+ # How many hours of data we need to sample, not to exceed 24h
1651
+ minus_hours = 3
1652
+
1653
+ # get unix cmd 'last' content
1654
+ begin
1655
+ content = `last`
1656
+ rescue
1657
+ warn "Failed to run 'last' command"
1658
+ return nil
1659
+ end
1660
+
1661
+
1662
+ counter = 0
1663
+
1664
+ (0..minus_hours).each do | minus_hour |
1665
+ target_time = Time.now - 60*60*minus_hour
1666
+ time_str = target_time.strftime("%b %d %H")
1667
+ content.split("\n").each do |line|
1668
+ counter += 1 if line =~ /#{time_str}/
1669
+ end
1670
+ end
1671
+ return counter
1672
+ end
1673
+
1674
+ # This is based on the perl version in OSInfo.pm
1675
+ def getdiskusage
1676
+ content = ""
1677
+ begin
1678
+ content = `df -k`
1679
+ rescue
1680
+ warn "Failed to run df command"
1681
+ return nil
1682
+ end
1683
+ used_space = 0
1684
+ avail_space = 0
1685
+ content.split("\n").each do |line|
1686
+ if line =~ /\s+\d+\s+(\d+)\s+(\d+)\s+\d+%\s+\/($|home$)/
1687
+ used_space += $1.to_i
1688
+ avail_space += $2.to_i
1689
+ end
1690
+ end
1691
+ return {:avail_space => avail_space, :used_space => used_space}
1692
+ end
1693
+
1694
+ def getvolumes
1695
+ return getmountedvolumes.merge(getservedvolumes)
1696
+ end
1697
+
1698
+ # This is based on the perl version in OSInfo.pm
1699
+ def getservedvolumes
1700
+ # only support Linux for now
1701
+ return {} unless Facter['kernel'] && Facter['kernel'].value == 'Linux'
1702
+
1703
+ # Don't do anything if exports file is not there
1704
+ return {} if !File.exists?("/etc/exports")
1705
+
1706
+ served = {}
1707
+
1708
+ IO.foreach("/etc/exports") do |line|
1709
+ if line =~ /(\S+)\s+/
1710
+ vol = $1
1711
+ served["volumes[served][#{vol}][config]"] = "/etc/exports"
1712
+ served["volumes[served][#{vol}][type]"] = 'nfs'
1713
+ end
1714
+ end
1715
+ return served
1716
+ end
1717
+
1718
+ # This is based on the perl version in OSInfo.pm
1719
+ def getmountedvolumes
1720
+ # only support Linux for now
1721
+ return {} unless Facter['kernel'] && Facter['kernel'].value == 'Linux'
1722
+
1723
+ dir = "/etc"
1724
+ mounted = {}
1725
+
1726
+ # AUTOFS - gather only files named auto[._]*
1727
+ Dir.glob(File.join(dir, "*")).each do |file|
1728
+ next if file !~ /^auto[._].*/
1729
+
1730
+ # AUTOFS - match only lines that look like nfs syntax such as host:/path
1731
+ IO.foreach(file) do |line|
1732
+ if line =~ /\w:\S/ && line !~ /^\s*#/
1733
+ # Parse it, Example : " nventory_backup -noatime,intr irvnetappbk:/vol/nventory_backup "
1734
+ if line =~ /^(\w[\w\S]+)\s+\S+\s+(\w[\w\S]+):(\S+)/
1735
+ mnt = $1
1736
+ host = $2
1737
+ vol = $3
1738
+ mounted["volumes[mounted][/mnt/#{mnt}][config]"] = file
1739
+ mounted["volumes[mounted][/mnt/#{mnt}][volume_server]"] = host
1740
+ mounted["volumes[mounted][/mnt/#{mnt}][volume]"] = vol
1741
+ mounted["volumes[mounted][/mnt/#{mnt}][type]"] = 'nfs'
1742
+ end
1743
+ end
1744
+ end # IO.foreach
1745
+ end # Dir.glob
1746
+
1747
+ # FSTAB - has diff syntax than AUTOFS. Example: "server:/usr/local/pub /pub nfs rsize=8192,wsize=8192,timeo=14,intr"
1748
+ IO.foreach("/etc/fstab") do |line|
1749
+ if line =~ /^(\w[\w\S]+):(\S+)\s+(\S+)\s+nfs/
1750
+ host = $1
1751
+ vol = $2
1752
+ mnt = $3
1753
+ mounted["volumes[mounted][#{mnt}][config]"] = "/etc/fstab"
1754
+ mounted["volumes[mounted][#{mnt}][volume_server]"] = host
1755
+ mounted["volumes[mounted][#{mnt}][volume]"] = vol
1756
+ mounted["volumes[mounted][#{mnt}][type]"] = 'nfs'
1757
+ end
1758
+ end # IO.foreach
1759
+ return mounted
1760
+ end
1761
+
1762
+ def getvmstatus
1763
+ # facter virtual makes calls to commands that are under /sbin
1764
+ vmstatus = Facter.virtual
1765
+ vmstatus.chomp!
1766
+
1767
+ # extra check to see if we're running kvm hypervisor
1768
+ os = Facter['kernel'].value
1769
+ if os == 'Linux'
1770
+ begin
1771
+ `grep ^kvm /proc/modules`
1772
+ vmstatus = "kvm_host" if $? == 0
1773
+ rescue
1774
+ warn "Failed to get modules information"
1775
+ end
1776
+ end
1777
+ return vmstatus
1778
+ end
1779
+
1780
+ # This is based on the perl version in HardwareInfo.pm
1781
+ def get_cpu_core_count
1782
+ # only support Linux for now
1783
+ os = Facter['kernel'].value
1784
+ physicalid = nil
1785
+ coreid = nil
1786
+ corecount = nil
1787
+ cores = {}
1788
+ if os == 'Linux'
1789
+ IO.foreach("/proc/cpuinfo") do |line|
1790
+ if line =~ /^processor\s*: (\d+)/
1791
+ physicalid = nil
1792
+ coreid = nil
1793
+ elsif line =~ /^physical id\s*: (\d+)/
1794
+ physicalid = $1
1795
+ elsif line =~ /^core id\s*: (\d+)/
1796
+ coreid = $1;
1797
+ end
1798
+ if physicalid && coreid
1799
+ cores["#{physicalid}:#{coreid}"] = 1;
1800
+ end
1801
+ end # IO.foreach
1802
+ corecount = cores.size
1803
+ end # if statement
1804
+ return corecount
1805
+ end
1806
+
1807
+ def get_console_type
1808
+ console_type = nil
1809
+ # only support Linux for now
1810
+ os = Facter['kernel'].value
1811
+ if os == 'Linux'
1812
+ if get_racadm
1813
+ console_type = "Dell DRAC"
1814
+ end
1815
+ end
1816
+ return console_type
1817
+ end
1818
+
1819
+ def get_drac_info
1820
+ info = {}
1821
+ result = nil
1822
+ racadm = get_racadm
1823
+ begin
1824
+ timeout(10) do
1825
+ cmd = "#{racadm} getsysinfo"
1826
+ result = `#{cmd}` || ""
1827
+ end
1828
+ result.split("\n").each do |line|
1829
+ if line =~ /^Current IP Address\s*=/i
1830
+ info[:ip_address] = line.split("=")[1].strip
1831
+ elsif line =~ /^MAC Address\s*=/i
1832
+ info[:mac_address] = line.split("=")[1].strip
1833
+ elsif line =~ /^DNS RAC Name\s*=/i
1834
+ info[:name] = line.split("=")[1].strip
1835
+ end
1836
+ end
1837
+ rescue Timeout::Error
1838
+ warn "Timed out when trying to get drac info"
1839
+ rescue Exception => e
1840
+ warn e.inspect
1841
+ warn "Failed to get DRAC IP"
1842
+ end
1843
+ return info
1844
+ end
1845
+
1846
+ def get_racadm
1847
+ path_env = (ENV['PATH'] || "").split(':')
1848
+ other_paths = ["/usr/sbin", "/opt/dell/srvadmin/sbin"]
1849
+ (path_env | other_paths).each do |path|
1850
+ if File.executable?(File.join(path, 'racadm'))
1851
+ return File.join(path, 'racadm')
1852
+ end
1853
+ end
1854
+ return nil
1855
+ end
1856
+
1857
+ def get_chassis_info
1858
+ chassis_info = {}
1859
+ manufacturer = nil
1860
+ # Only support Dell hardware for now
1861
+ if Facter['manufacturer'] && Facter['manufacturer'].value
1862
+ manufacturer = Facter['manufacturer'].value
1863
+ if manufacturer =~ /Dell/
1864
+ chassis_info = get_dell_chassis_info
1865
+ end
1866
+ end
1867
+ return chassis_info
1868
+ end
1869
+
1870
+ # call Dell's omreport command to get service tag
1871
+ # of the chassis, and the slot value of where the
1872
+ # given node resides in. Result is stored in hash with
1873
+ # service_tag and slot_num as the keys
1874
+ def get_dell_chassis_info
1875
+ ENV['PATH'] = "#{ENV['PATH']}:/opt/dell/srvadmin/bin/"
1876
+ chassis = {}
1877
+ result = nil
1878
+ begin
1879
+ #result = `omreport modularenclosure -fmt ssv`
1880
+ #result.split("\n").each do |line|
1881
+ # if line =~ /Service Tag/
1882
+ # chassis[:service_tag] = line.split(";")[1].strip
1883
+ # break
1884
+ # end
1885
+ #end
1886
+ timeout(5) do
1887
+ result = `omreport chassis info -fmt ssv`
1888
+ end
1889
+ result.split("\n").each do |line|
1890
+ if line =~ /Server Module Location;Slot (\d+)/
1891
+ chassis[:slot_num] = $1.to_i
1892
+ elsif line =~ /Chassis Service Tag/
1893
+ chassis[:service_tag] = line.split(";")[1].strip
1894
+ end
1895
+ end
1896
+ # if no slot_number then the blade isn't really in a chassis/blade enclosure
1897
+ # such as the case with Dell PowerEdge 1950
1898
+ return {} if chassis[:slot_num].nil?
1899
+ rescue Timeout::Error
1900
+ warn "Timed out when trying to run omreport"
1901
+ rescue
1902
+ warn "Failed to run/parse Dell's omreport command"
1903
+ end
1904
+ return chassis
1905
+ end
1906
+
1907
+ # Currently, the only info this method gathers is the info
1908
+ # of the guests running on this kvm host
1909
+ def get_kvm_hostinfo
1910
+ guests = {}
1911
+ begin
1912
+ result = `virsh list --all`
1913
+ result.split("\n").each do |line|
1914
+ if line =~ /(\d+)\s+(\S+)\s+(\S+)/
1915
+ guest_hostname = $2
1916
+ guests[guest_hostname] = get_kvm_guest_info(guest_hostname)
1917
+ end
1918
+ end
1919
+ rescue
1920
+ warn "Failed to run/parse virsh command"
1921
+ end
1922
+ return guests
1923
+ end
1924
+
1925
+ # Currently, the only info this method gathers is the
1926
+ # image size
1927
+ def get_kvm_guest_info(guest)
1928
+ info = {}
1929
+ result = `virsh dumpxml #{guest}`
1930
+ result.split("\n").each do |line|
1931
+ if line =~ /source file='(.+)'/
1932
+ img_path = $1
1933
+ if File.exists?(img_path)
1934
+ # nVentory expects the value to be in KB
1935
+ info['vmimg_size'] = File.stat(img_path).size.to_i / 1024
1936
+ # how to calculate this?
1937
+ # info['vmspace_used'] = ???
1938
+ end
1939
+ end
1940
+ end
1941
+ return info.clone
1942
+ end
1943
+
1944
+ # Most of the code in this method are based on the code of the
1945
+ # perl nVentory client
1946
+ def get_nic_info
1947
+ info = {}
1948
+ os = Facter['kernel'].value
1949
+ # only support Linux right now
1950
+ return info if os != 'Linux'
1951
+
1952
+ nic = nil
1953
+ result = `/sbin/ifconfig -a`
1954
+ result.split("\n").each do |line|
1955
+ if line =~ /^(\w+\S+)/
1956
+ nic = $1
1957
+ info[nic] = {}
1958
+ end
1959
+ if line =~ /(?:HWaddr|ether) ([\da-fA-F:]+)/
1960
+ info[nic][:hardware_address] = $1
1961
+ if line =~ /ether/i
1962
+ info[nic][:interface_type] = 'Ethernet'
1963
+ end
1964
+ elsif line =~ /^\s+UP / || line =~ /flags=.*UP,/
1965
+ info[nic][:up] = 1
1966
+ end
1967
+ end
1968
+
1969
+ # Get additional info
1970
+ info.each do |nic, nic_info|
1971
+ next if nic_info[:interface_type] != 'Ethernet'
1972
+ next if nic =~ /virbr|veth|vif|peth/
1973
+ result = `/sbin/ethtool #{nic}`
1974
+ result.split("\n").each do |line|
1975
+ if line =~ /Speed: (\d+)Mb/
1976
+ nic_info[:speed] = $1
1977
+ elsif line =~ /Duplex: (\w+)/
1978
+ ($1.downcase == 'full')? nic_info[:full_duplex] = 1 : nic_info[:full_duplex] = 0
1979
+ elsif line =~ /Advertised auto-negotiation: (.*)/
1980
+ ($1.downcase == 'yes')? nic_info[:autonegotiate] = 1 : nic_info[:autonegotiate] = 0
1981
+ elsif line =~ /Link detected: (\w+)/
1982
+ ($1.downcase == 'yes')? nic_info[:link] = 1: nic_info[:link] = 0
1983
+ end
1984
+ end
1985
+ end
1986
+ return info
1987
+ end
1988
+
1989
+ # Parse dmidecode data and put it into a hash
1990
+ # This method is based on the corresponding method in the perl client
1991
+ def get_dmi_data
1992
+ return @dmi_data if @dmi_data
1993
+
1994
+ case Facter.value(:kernel)
1995
+ when 'Linux'
1996
+ return nil unless FileTest.exists?("/usr/sbin/dmidecode")
1997
+
1998
+ output=%x{/usr/sbin/dmidecode 2>/dev/null}
1999
+ when 'FreeBSD'
2000
+ return nil unless FileTest.exists?("/usr/local/sbin/dmidecode")
2001
+
2002
+ output=%x{/usr/local/sbin/dmidecode 2>/dev/null}
2003
+ when 'NetBSD'
2004
+ return nil unless FileTest.exists?("/usr/pkg/sbin/dmidecode")
2005
+
2006
+ output=%x{/usr/pkg/sbin/dmidecode 2>/dev/null}
2007
+ when 'SunOS'
2008
+ return nil unless FileTest.exists?("/usr/sbin/smbios")
2009
+
2010
+ output=%x{/usr/sbin/smbios 2>/dev/null}
2011
+ else
2012
+ warn "Can't get dmi_data because of unsupported OS"
2013
+ return
2014
+ end
2015
+
2016
+ look_for_section_name = false
2017
+ dmi_section = nil
2018
+ dmi_section_data = {}
2019
+ dmi_section_array = nil
2020
+ @dmi_data = {}
2021
+
2022
+ output.split("\n").each do |line|
2023
+ if line =~ /^Handle/
2024
+ if dmi_section && !dmi_section_data.empty?
2025
+ @dmi_data[dmi_section] ||= []
2026
+ @dmi_data[dmi_section] << dmi_section_data
2027
+ end
2028
+ dmi_section = nil
2029
+ dmi_section_data = {}
2030
+ dmi_section_array = nil
2031
+ look_for_section_name = true
2032
+ elsif look_for_section_name
2033
+ next if line =~ /^\s*DMI type/
2034
+ if line =~ /^\s*(.*)/
2035
+ dmi_section = $1
2036
+ look_for_section_name = false
2037
+ end
2038
+ elsif dmi_section && line =~ /^\s*([^:]+):\s*(\S.*)/
2039
+ dmi_section_data[$1] = $2;
2040
+ dmi_section_array = nil
2041
+ elsif dmi_section && line =~ /^\s*([^:]+):$/
2042
+ dmi_section_array = $1
2043
+ elsif dmi_section && dmi_section_array && line =~ /^\s*(\S.+)$/
2044
+ dmi_section_data[dmi_section_array] ||= []
2045
+ dmi_section_data[dmi_section_array] << $1
2046
+ end
2047
+ end
2048
+ @dmi_data
2049
+ end
2050
+
2051
+ # This method is based on the one in the perl client
2052
+ def get_physical_memory
2053
+ # only support Linux and FreeBSD right now
2054
+ os = Facter['kernel']
2055
+ return if os.nil? or (os.value != 'Linux' and os.value != 'FreeBSD')
2056
+
2057
+ physical_memory = 0
2058
+ dmi_data = get_dmi_data
2059
+
2060
+ return if dmi_data.nil? or dmi_data['Memory Device'].nil?
2061
+
2062
+ dmi_data['Memory Device'].each do |mem_dev|
2063
+
2064
+ size = mem_dev['Size']
2065
+ form_factor = mem_dev['Form Factor']
2066
+ locator = mem_dev['Locator']
2067
+ # Some systems report little chunks of memory other than
2068
+ # main system memory as Memory Devices, the 'DIMM' as
2069
+ # form factor seems to indicate main system memory.
2070
+ # Unfortunately some DIMMs are reported with a form
2071
+ # factor of '<OUT OF SPEC>'. In that case fall back to
2072
+ # checking for signs of it being a DIMM in the locator
2073
+ # field.
2074
+ if (size != 'No Module Installed' &&
2075
+ ((form_factor == 'DIMM' || form_factor == 'FB-DIMM' || form_factor == 'SODIMM') ||
2076
+ (form_factor == '<OUT OF SPEC>' && locator =~ /DIMM/)))
2077
+ megs, units = size.split(' ')
2078
+
2079
+ next if units != 'MB'
2080
+ physical_memory += megs.to_i;
2081
+ end
2082
+ end
2083
+ physical_memory
2084
+ end
2085
+
2086
+ end