module ForemanSalt class ReportImporter delegate :logger, to: :Rails attr_reader :report def self.import(raw, proxy_id = nil) raise ::Foreman::Exception, _('Invalid report') unless raw.is_a?(Hash) raw.map do |host, report| importer = ForemanSalt::ReportImporter.new(host, report, proxy_id) importer.import report = importer.report report.origin = 'Salt' report.save! report end end def initialize(host, raw, proxy_id = nil) @host = find_or_create_host(host) @raw = raw @proxy_id = proxy_id end def import logger.info "processing report for #{@host}" logger.debug { "Report: #{@raw.inspect}" } if @host.new_record? && !Setting[:create_new_host_when_report_is_uploaded] logger.info("skipping report for #{@host} as its an unknown host and create_new_host_when_report_is_uploaded setting is disabled") return ConfigReport.new end @host.salt_proxy_id ||= @proxy_id @host.last_report = start_time if [Array, String].member? @raw.class process_failures # If Salt sends us only an array (or string), it's a list of fatal failures else process_normal end @host.save(validate: false) @host.reload @host.refresh_statuses([HostStatus.find_status_by_humanized_name('configuration')]) logger.info("Imported report for #{@host} in #{(Time.zone.now - start_time).round(2)} seconds") end private def find_or_create_host(host) @host ||= Host::Managed.find_by(name: host) unless @host new = Host::Managed.new(name: host) new.save(validate: false) @host = new end @host end def import_log_messages @raw.each do |resource, result| level = if result['changes'].blank? && result['result'] :info elsif result['result'] == false :err else # nil mean "unchanged" when running highstate with test=True :notice end source = Source.find_or_create_by(value: resource) message = if result['changes']['diff'] result['changes']['diff'] elsif result['pchanges'].present? && result['pchanges'].include?('diff') result['pchanges']['diff'] elsif result['comment'].presence result['comment'] else 'No message available' end message = Message.find_or_create_by(value: message) Log.create(message_id: message.id, source_id: source.id, report: @report, level: level) end end def calculate_metrics success = 0 failed = 0 changed = 0 restarted = 0 restarted_failed = 0 pending = 0 time = {} @raw.each do |resource, result| next unless result.is_a? Hash if result['result'] success += 1 if resource.match(/^service_/) && result['comment'].include?('restarted') restarted += 1 elsif result['changes'].present? changed += 1 elsif result['pchanges'].present? pending += 1 end elsif result['result'].nil? pending += 1 elsif !result['result'] if resource.match(/^service_/) && result['comment'].include?('restarted') restarted_failed += 1 else failed += 1 end end duration = if result['duration'].is_a? String begin Float(result['duration'].delete(' ms')) rescue StandardError nil end else result['duration'] end # Convert duration from milliseconds to seconds duration /= 1000 if duration.is_a? Float time[resource] = duration || 0 end time[:total] = time.values.compact.sum || 0 events = { total: changed + failed + restarted + restarted_failed, success: success + restarted, failure: failed + restarted_failed } changes = { total: changed + restarted } resources = { 'total' => @raw.size, 'applied' => changed, 'restarted' => restarted, 'failed' => failed, 'failed_restarts' => restarted_failed, 'skipped' => 0, 'scheduled' => 0, 'pending' => pending } { events: events, resources: resources, changes: changes, time: time } end def process_normal metrics = calculate_metrics status = ConfigReportStatusCalculator.new(counters: metrics[:resources].slice(*::ConfigReport::METRIC)).calculate @report = ConfigReport.new(host: @host, reported_at: start_time, status: status, metrics: metrics) return @report unless @report.save import_log_messages end def process_failures @raw = [@raw] unless @raw.is_a? Array status = ConfigReportStatusCalculator.new(counters: { 'failed' => @raw.size }).calculate @report = ConfigReport.create(host: @host, reported_at: Time.zone.now, status: status, metrics: {}) source = Source.find_or_create_by(value: 'Salt') @raw.each do |failure| message = Message.find_or_create_by(value: failure) Log.create(message_id: message.id, source_id: source.id, report: @report, level: :err) end end def start_time @start_time ||= Time.zone.now end end end