nventory-client 1.65.4

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