# copy this file to your report dir - e.g. /usr/lib/ruby/1.8/puppet/reports/ # add this report in your puppetmaster reports - e.g, in your puppet.conf add: # reports=log, foreman # (or any other reports you want) # URL of your Foreman installation $foreman_url='<%= @foreman_url %>' # if CA is specified, remote Foreman host will be verified $foreman_ssl_ca = "<%= @ssl_ca -%>" # ssl_cert and key are required if require_ssl_puppetmasters is enabled in Foreman $foreman_ssl_cert = "<%= @ssl_cert -%>" $foreman_ssl_key = "<%= @ssl_key -%>" require 'puppet' require 'net/http' require 'net/https' require 'uri' begin require 'json' rescue LoadError # Debian packaging guidelines state to avoid needing rubygems, so # we only try to load it if the first require fails (for RPMs) begin require 'rubygems' rescue nil require 'json' rescue LoadError => e puts "You need the `json` gem to use the Foreman ENC script" # code 1 is already used below exit 2 end end Puppet::Reports.register_report(:foreman) do Puppet.settings.use(:reporting) desc "Sends reports directly to Foreman" def process begin # check for report metrics raise(Puppet::ParseError, "Invalid report: can't find metrics information for #{self.host}") if self.metrics.nil? uri = URI.parse($foreman_url) http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = uri.scheme == 'https' if http.use_ssl? if $foreman_ssl_ca && !$foreman_ssl_ca.empty? http.ca_file = $foreman_ssl_ca http.verify_mode = OpenSSL::SSL::VERIFY_PEER else http.verify_mode = OpenSSL::SSL::VERIFY_NONE end if $foreman_ssl_cert && !$foreman_ssl_cert.empty? && $foreman_ssl_key && !$foreman_ssl_key.empty? http.cert = OpenSSL::X509::Certificate.new(File.read($foreman_ssl_cert)) http.key = OpenSSL::PKey::RSA.new(File.read($foreman_ssl_key), nil) end end req = Net::HTTP::Post.new("#{uri.path}/api/reports") req.add_field('Accept', 'application/json,version=2' ) req.content_type = 'application/json' req.body = {'report' => generate_report}.to_json response = http.request(req) rescue Exception => e raise Puppet::Error, "Could not send report to Foreman at #{$foreman_url}/api/reports: #{e}\n#{e.backtrace}" end end def generate_report report = {} set_report_format report['host'] = self.host # Time.to_s behaves differently in 1.8 / 1.9 so we explicity set the 1.9 format report['reported_at'] = self.time.utc.strftime("%Y-%m-%d %H:%M:%S UTC") report['status'] = metrics_to_hash(self) report['metrics'] = m2h(self.metrics) report['logs'] = logs_to_array(self.logs) report end private METRIC = %w[applied restarted failed failed_restarts skipped pending] def metrics_to_hash(report) report_status = {} metrics = self.metrics # find our metric values METRIC.each do |m| if @format == 0 report_status[m] = metrics["resources"][m.to_sym] unless metrics["resources"].nil? else h=translate_metrics_to26(m) mv = metrics[h[:type]] report_status[m] = mv[h[:name].to_sym] + mv[h[:name].to_s] rescue nil end report_status[m] ||= 0 end # special fix for false warning about skips # sometimes there are skip values, but there are no error messages, we ignore them. if report_status["skipped"] > 0 and ((report_status.values.inject(:+)) - report_status["skipped"] == report.logs.size) report_status["skipped"] = 0 end # fix for reports that contain no metrics (i.e. failed catalog) if @format > 1 and report.respond_to?(:status) and report.status == "failed" report_status["failed"] += 1 end # fix for Puppet non-resource errors (i.e. failed catalog fetches before falling back to cache) report_status["failed"] += report.logs.find_all {|l| l.source == 'Puppet' && l.level.to_s == 'err' }.count return report_status end def m2h metrics h = {} metrics.each do |title, mtype| h[mtype.name] ||= {} mtype.values.each{|m| h[mtype.name].merge!({m[0].to_s => m[2]})} end return h end def logs_to_array logs h = [] logs.each do |log| # skipping debug messages, we dont want them in Foreman's db next if log.level == :debug # skipping catalog summary run messages, we dont want them in Foreman's db next if log.message =~ /^Finished catalog run in \d+.\d+ seconds$/ # Match Foreman's slightly odd API format... l = { 'log' => { 'sources' => {}, 'messages' => {} } } l['log']['level'] = log.level.to_s l['log']['messages']['message'] = log.message l['log']['sources']['source'] = log.source h << l end return h end # The metrics layout has changed in Puppet 2.6.x release, # this method attempts to align the bit value metrics and the new name scheme in 2.6.x # returns a hash of { :type => "metric type", :name => "metric_name"} def translate_metrics_to26 metric case metric when "applied" case @format when 0..1 { :type => "total", :name => :changes} else { :type => "changes", :name => "total"} end when "failed_restarts" case @format when 0..1 { :type => "resources", :name => metric} else { :type => "resources", :name => "failed_to_restart"} end when "pending" { :type => "events", :name => "noop" } else { :type => "resources", :name => metric} end end def set_report_format @format ||= case when self.instance_variables.detect {|v| v.to_s == "@environment"} @format = 3 when self.instance_variables.detect {|v| v.to_s == "@report_format"} @format = 2 when self.instance_variables.detect {|v| v.to_s == "@resource_statuses"} @format = 1 else @format = 0 end end end