lib/nventory.rb in nventory-client-1.65.4 vs lib/nventory.rb in nventory-client-1.67

- old
+ new

@@ -1,21 +1,38 @@ +# $Id: nventory.rb 343 2013-02-15 23:09:46Z saltmanm $ begin # Try loading facter w/o gems first so that we don't introduce a # dependency on gems if it is not needed. require 'facter' # Facter rescue LoadError require 'rubygems' require 'facter' end +require 'facter/util/memory' # used for converting MB to GB and stuff require 'uri' require 'net/http' require 'net/https' require 'cgi' require 'rexml/document' require 'yaml' -# fix for ruby http bug where it encodes the params incorrectly +# Only use json gem if its there +begin + require 'rubygems' + require 'json' + HAS_JSON_GEM = true +rescue LoadError + HAS_JSON_GEM = false +end + +# +# 1.9 compatible solution for ruby 1.8 net/http bug that improperly encodes params in an array. +# Example: +# params = {:param1 => ["1", "2", "3"]} +# bad = param1=123 +# good = param1=1&param1=2&param1=3 +# class Net::HTTP::Put def set_form_data(params, sep = '&') params_array = params.map do |k,v| if v.is_a? Array v.inject([]){|c, val| c << "#{urlencode(k.to_s)}=#{urlencode(val.to_s)}"}.join(sep) @@ -23,11 +40,11 @@ "#{urlencode(k.to_s)}=#{urlencode(v.to_s)}" end end self.body = params_array.join(sep) self.content_type = 'application/x-www-form-urlencoded' - end + end if method_defined? :urlencode end module PasswordCallback @@password = nil def self.get_password @@ -69,12 +86,12 @@ parms[:sso_server] ? (@sso_server = parms[:sso_server]) : (@sso_server = nil) parms[:configfile] ? (configfile = parms[:configfile]) : (configfile = nil) end @ca_file = nil @ca_path = nil - @dhparams = '/etc/nventory/dhparams' @delete = false # Initialize the variable, see attr_accessor above + @dmi_data = nil CONFIG_FILES << configfile if configfile CONFIG_FILES.each do |configfile| if File.exist?(configfile) @@ -99,13 +116,10 @@ @ca_file = value warn "Using ca_file #{@ca_file} from #{configfile}" if (@debug) elsif key == 'ca_path' @ca_path = value warn "Using ca_path #{@ca_path} from #{configfile}" if (@debug) - elsif key == 'dhparams' - @dhparams = value - warn "Using dhparams #{@dhparams} from #{configfile}" if (@debug) elsif key == 'cookiefile' @cookiefile = value warn "Using cookiefile #{@cookiefile} from #{configfile}" if (@debug) end end @@ -298,11 +312,22 @@ # # Send the query to the server # - uri = URI::join(@server, "#{objecttype}.xml?#{querystring}") + if parms[:format] == 'json' + if HAS_JSON_GEM + uri = URI::join(@server, "#{objecttype}.json?#{querystring}") + else + warn "Warning: Cannot use json format because json gem is not installed. Using xml format instead." + parms[:format] = 'xml' + uri = URI::join(@server, "#{objecttype}.xml?#{querystring}") + end + else + uri = URI::join(@server, "#{objecttype}.xml?#{querystring}") + end + req = Net::HTTP::Get.new(uri.request_uri) warn "GET URL: #{uri}" if (@debug) response = send_request(req, uri, login, password_callback) while response.kind_of?(Net::HTTPMovedPermanently) uri = URI.parse(response['Location']) @@ -312,30 +337,32 @@ if !response.kind_of?(Net::HTTPOK) puts response.body response.error! end - # - # Parse the XML data from the server - # This tries to render the XML into the best possible representation - # as a Perl hash. It may need to evolve over time. - # - - puts response.body if (@debug) - results_xml = REXML::Document.new(response.body) - results = {} - if results_xml.root.elements["/#{objecttype}"] - results_xml.root.elements["/#{objecttype}"].each do |elem| - # For some reason Elements[] is returning things other than elements, - # like text nodes - next if elem.node_type != :element - data = xml_to_ruby(elem) - name = data['name'] || data['id'] - if !results[name].nil? - warn "Duplicate entries for #{name}. Only one will be shown." + if parms[:format] == 'json' + results = JSON.parse(response.body) + else + # + # Parse the XML data from the server + # This tries to render the XML into the best possible representation + # as a Perl hash. It may need to evolve over time. + puts response.body if (@debug) + results_xml = REXML::Document.new(response.body) + results = {} + if results_xml.root.elements["/#{objecttype}"] + results_xml.root.elements["/#{objecttype}"].each do |elem| + # For some reason Elements[] is returning things other than elements, + # like text nodes + next if elem.node_type != :element + data = xml_to_ruby(elem) + name = data['name'] || data['id'] + if !results[name].nil? + warn "Duplicate entries for #{name}. Only one will be shown." + end + results[name] = data end - results[name] = data end end #puts results.inspect if (@debug) puts YAML.dump(results) if (@debug) @@ -539,15 +566,15 @@ else # Not sure if this is reasonable data['operating_system[architecture]'] = Facter['hardwaremodel'].value end data['kernel_version'] = Facter['kernelrelease'].value - if Facter['memorysize'] && Facter['memorysize'].value - data['os_memory'] = Facter['memorysize'].value - elsif Facter['sp_physical_memory'] && Facter['sp_physical_memory'].value # Mac OS X + if Facter.value('memorysize') + data['os_memory'] = Facter.value('memorysize') + elsif Facter.value('sp_physical_memory') # Mac OS X # More or less a safe bet that OS memory == physical memory on Mac OS X - data['os_memory'] = Facter['sp_physical_memory'].value + data['os_memory'] = Facter.value('sp_physical_memory') end if Facter['swapsize'] data['swap'] = Facter['swapsize'].value end # Currently the processorcount fact doesn't even get defined on most platforms @@ -632,13 +659,15 @@ end data['processor_core_count'] = get_cpu_core_count #data['processor_socket_count'] = #data['power_supply_count'] = - #data['physical_memory'] = #data['physical_memory_sizes'] = + physical_memory = get_physical_memory + data['physical_memory'] = Facter::Memory.scale_number(physical_memory, "MB") if physical_memory + nics = [] if Facter['interfaces'] && Facter['interfaces'].value nics = Facter['interfaces'].value.split(',') nics.each do |nic| data["network_interfaces[#{nic}][name]"] = nic @@ -771,23 +800,35 @@ getdata[:exactget] = {'uniqueid' => [reverse_uniqueid]} results = get_objects(getdata) end end - # If we failed to find an existing entry based on the unique id - # fall back to the hostname. This may still fail to find an entry, - # if this is a new host, but that's OK as it will leave %results - # as undef, which triggers set_nodes to create a new entry on the - # server. + # If we failed to find an existing entry based on the unique id, + # fall back to the hostname. if results.empty? && data['name'] getdata = {} getdata[:objecttype] = 'nodes' getdata[:exactget] = {'name' => [data['name']]} getdata[:login] = 'autoreg' results = get_objects(getdata) end + # If we failed to find an existing entry based on the uniqueid and hostname, + # fall back to using serial number. This may still fail to find an entry, + # if this is a new host, but that's OK as it will leave %results + # as undef, which triggers set_nodes to create a new entry on the + # server. + if results.empty? && data['serial_number'] && + !data['serial_number'].empty? && + data['serial_number'] != "Not Specified" + getdata = {} + getdata[:objecttype] = 'nodes' + getdata[:exactget] = {'serial_number' => [data['serial_number']]} + getdata[:login] = 'autoreg' + results = get_objects(getdata) + end + setresults = set_objects('nodes', results, data, 'autoreg') puts "Command successful" if setresults == 1 end # Add the given node into the given nodegroup by directly @@ -898,13 +939,13 @@ parent_groups.each_pair do |parent_group_name, parent_group| # Use a hash to merge the current and new members and # eliminate duplicates merged_nodegroups = child_groups - if parent_group[child_groups] - parent_group[child_groups].each do |child_group| - name = child_group[name] + if parent_group['child_groups'] + parent_group['child_groups'].each do |child_group| + name = child_group['name'] merged_nodegroups[name] = child_group end end set_nodegroup_nodegroup_assignments(merged_nodegroups, {parent_group_name => parent_group}, login, password_callback) @@ -921,22 +962,23 @@ # FIXME: This should talk directly to the node_group_node_groups_assignments # controller, so that we aren't exposed to the race conditions this # method currently suffers from. parent_groups.each_pair do |parent_group_name, parent_group| desired_child_groups = {} - if parent_groups[child_groups] - parent_group[child_groups].each do |child_group| - name = child_group[name] + if parent_group['child_groups'] + parent_group['child_groups'].each do |child_group| + name = child_group['name'] if !child_groups.has_key?(name) desired_child_groups[name] = child_group end end end set_nodegroup_nodegroup_assignments(desired_child_groups, {parent_group_name => parent_group}, login, password_callback) end end + # Both arguments are hashes returned by a 'node_groups' call to get_objects def set_nodegroup_nodegroup_assignments(child_groups, parent_groups, login, password_callback=PasswordCallback) child_ids = [] child_groups.each_pair do |child_group_name, child_group| if child_group['id'] @@ -991,10 +1033,64 @@ else delete_objects('taggings', taggings_to_del, login, password_callback=PasswordCallback) end end + # Add a new graffiti to given objects. We're assuming that graffiti is a string + # of "key:value" format + # obj_type is a string that describe the type of the obj (e.g NodeGroup) + # obj_hash is the hash returned from calling get_objects + def add_graffiti(obj_type, obj_hash, graffiti, login, password_callback=PasswordCallback) + name,value = graffiti.split(':') + obj_hash.each_value do |obj| + set_objects('graffitis', nil, + { :name => name, + :value => value, + :graffitiable_id => obj['id'], + :graffitiable_type => obj_type, + }, + login, password_callback); + end + end + + # Delete the graffiti (based on the name) from the given objects + # obj_type is a string that describe the type of the obj (e.g NodeGroup) + # obj_hash is the hash returned from calling get_objects + def delete_graffiti(obj_type, obj_hash, graffiti_name, login, password_callback=PasswordCallback) + obj_hash.each_value do |obj| + getdata = {:objecttype => 'graffitis', + :exactget => {:name => graffiti_name, + :graffitiable_id => obj['id'], + :graffitiable_type => obj_type} + } + graffitis_to_delete = get_objects(getdata) + delete_objects('graffitis', graffitis_to_delete, login, password_callback) + end + end + + def get_service_tree(service_name) + getdata = {} + getdata[:objecttype] = 'services' + getdata[:exactget] = {'name' => [service_name]} + getdata[:includes] = ['nodes', 'parent_services'] + services = {service_name => []} + results = get_objects(getdata) + + if results.has_key?(service_name) + if results[service_name].has_key?('parent_services') && !results[service_name]['parent_services'].empty? + results[service_name]['parent_services'].each do |service| + services[service_name] << get_service_tree(service['name']) + end + else + return service_name + end + else # no such service + return {} + end + return services + end + # # Helper methods # def self.get_uniqueid os = Facter['kernel'].value @@ -1037,11 +1133,15 @@ end uuid_entry = `/usr/sbin/dmidecode | grep UUID` if uuid_entry uuid = uuid_entry.split(":")[1] end - return uuid.strip + if uuid + return uuid.strip + else + return nil + end end def self.get_hardware_profile result = {:manufacturer => 'Unknown', :model => 'Unknown'} if Facter['manufacturer'] && Facter['manufacturer'].value # dmidecode @@ -1068,16 +1168,10 @@ http = proxy.new(uri.host, uri.port) else http = Net::HTTP.new(uri.host, uri.port) end if uri.scheme == "https" - # Eliminate the OpenSSL "using default DH parameters" warning - if File.exist?(@dhparams) - dh = OpenSSL::PKey::DH.new(IO.read(@dhparams)) - Net::HTTP.ssl_context_accessor(:tmp_dh_callback) - http.tmp_dh_callback = proc { dh } - end http.use_ssl = true if @ca_file && File.exist?(@ca_file) http.ca_file = @ca_file http.verify_mode = OpenSSL::SSL::VERIFY_PEER end @@ -1210,18 +1304,26 @@ cookies end # Extract cookie from response and save it to the user's cookie store def extract_cookie(response, uri, login=nil) - if response['set-cookie'] - cookiefile = get_cookiefile(login) + # response['set-cookie'] returns multiple cookies comma-separated, + # which is difficult to parse since Expires field also uses comma in it. + # Fortunately, response.get_fields('set-cookie') returns an array of cookies. + cookies = response.get_fields('set-cookie') + if cookies.nil? + puts "extract_cookie finds no cookie in response" if (@debug) + return + end + cookiefile = get_cookiefile(login) + cookies.each do |one_cookie| # It doesn't look like it matters for our purposes at the moment, but # according to rfc2965, 3.2.2 the Set-Cookie header can contain more # than one cookie, separated by commas. - puts "extract_cookie processing #{response['set-cookie']}" if (@debug) - newcookie = parse_cookie('Set-Cookie: ' + response['set-cookie']) - return if newcookie.nil? + puts "extract_cookie processing #{one_cookie}" if (@debug) + newcookie = parse_cookie('Set-Cookie: ' + one_cookie) + next if newcookie.nil? # Some cookie fields are optional, and should default to the # values in the request. We need to insert these so that we # save them properly. # http://cgi.netscape.com/newsref/std/cookie_spec.html @@ -1265,12 +1367,10 @@ puts "Updating cookiefile #{cookiefile}" if (@debug) File.open(cookiefile, 'w') { |file| file.puts(cookies.collect{|cookie| cookie[:line]}.join("\n")) } else puts "No cookie changes, leaving cookiefile untouched" if (@debug) end - else - puts "extract_cookie finds no cookie in response" if (@debug) end end # Sends requests to the nVentory server and handles any redirects to # authentication pages or services. @@ -1662,12 +1762,11 @@ return mounted end def getvmstatus # facter virtual makes calls to commands that are under /sbin - ENV['PATH'] = "#{ENV['PATH']}:/sbin" - vmstatus = `facter virtual` + vmstatus = Facter.virtual vmstatus.chomp! # extra check to see if we're running kvm hypervisor os = Facter['kernel'].value if os == 'Linux' @@ -1887,6 +1986,104 @@ end end end return info end + + # Parse dmidecode data and put it into a hash + # This method is based on the corresponding method in the perl client + def get_dmi_data + return @dmi_data if @dmi_data + + case Facter.value(:kernel) + when 'Linux' + return nil unless FileTest.exists?("/usr/sbin/dmidecode") + + output=%x{/usr/sbin/dmidecode 2>/dev/null} + when 'FreeBSD' + return nil unless FileTest.exists?("/usr/local/sbin/dmidecode") + + output=%x{/usr/local/sbin/dmidecode 2>/dev/null} + when 'NetBSD' + return nil unless FileTest.exists?("/usr/pkg/sbin/dmidecode") + + output=%x{/usr/pkg/sbin/dmidecode 2>/dev/null} + when 'SunOS' + return nil unless FileTest.exists?("/usr/sbin/smbios") + + output=%x{/usr/sbin/smbios 2>/dev/null} + else + warn "Can't get dmi_data because of unsupported OS" + return + end + + look_for_section_name = false + dmi_section = nil + dmi_section_data = {} + dmi_section_array = nil + @dmi_data = {} + + output.split("\n").each do |line| + if line =~ /^Handle/ + if dmi_section && !dmi_section_data.empty? + @dmi_data[dmi_section] ||= [] + @dmi_data[dmi_section] << dmi_section_data + end + dmi_section = nil + dmi_section_data = {} + dmi_section_array = nil + look_for_section_name = true + elsif look_for_section_name + next if line =~ /^\s*DMI type/ + if line =~ /^\s*(.*)/ + dmi_section = $1 + look_for_section_name = false + end + elsif dmi_section && line =~ /^\s*([^:]+):\s*(\S.*)/ + dmi_section_data[$1] = $2; + dmi_section_array = nil + elsif dmi_section && line =~ /^\s*([^:]+):$/ + dmi_section_array = $1 + elsif dmi_section && dmi_section_array && line =~ /^\s*(\S.+)$/ + dmi_section_data[dmi_section_array] ||= [] + dmi_section_data[dmi_section_array] << $1 + end + end + @dmi_data + end + + # This method is based on the one in the perl client + def get_physical_memory + # only support Linux and FreeBSD right now + os = Facter['kernel'] + return if os.nil? or (os.value != 'Linux' and os.value != 'FreeBSD') + + physical_memory = 0 + dmi_data = get_dmi_data + + return if dmi_data.nil? or dmi_data['Memory Device'].nil? + + dmi_data['Memory Device'].each do |mem_dev| + + size = mem_dev['Size'] + form_factor = mem_dev['Form Factor'] + locator = mem_dev['Locator'] + # Some systems report little chunks of memory other than + # main system memory as Memory Devices, the 'DIMM' as + # form factor seems to indicate main system memory. + # Unfortunately some DIMMs are reported with a form + # factor of '<OUT OF SPEC>'. In that case fall back to + # checking for signs of it being a DIMM in the locator + # field. + if (size != 'No Module Installed' && + ((form_factor == 'DIMM' || form_factor == 'FB-DIMM' || form_factor == 'SODIMM') || + (form_factor == '<OUT OF SPEC>' && locator =~ /DIMM/))) + megs, units = size.split(' ') + + next if units != 'MB' + physical_memory += megs.to_i; + end + end + physical_memory + end + end