theforeman/foreman_salt

View on GitHub
app/services/foreman_salt/report_importer.rb

Summary

Maintainability
B
6 hrs
Test Coverage
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