nv-client 1.66.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/Gemfile +4 -0
- data/README +5 -0
- data/Rakefile +2 -0
- data/bin/nv +709 -0
- data/lib/nv-client.rb +5 -0
- data/lib/nv-client/version.rb +5 -0
- data/lib/nventory.rb +2086 -0
- data/nv-client.gemspec +22 -0
- metadata +85 -0
data/lib/nv-client.rb
ADDED
data/lib/nventory.rb
ADDED
@@ -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
|