theforeman/foreman

View on GitHub
app/services/report_importer.rb

Summary

Maintainability
A
1 hr
Test Coverage
class ReportImporter
  include Foreman::TelemetryHelper

  delegate :logger, :to => :Rails
  attr_reader :report, :report_scanners

  # When writing your own Report importer, provide feature(s) of authorized Smart Proxies
  # via ReportImporter.register_smart_proxy_feature method. Do not override this method!
  def self.authorized_smart_proxy_features
    @authorized_smart_proxy_features ||= []
  end

  def self.register_smart_proxy_feature(feature)
    @authorized_smart_proxy_features = (authorized_smart_proxy_features + [feature.freeze]).uniq
  end

  def self.unregister_smart_proxy_feature(feature)
    @authorized_smart_proxy_features -= [feature]
  end

  def self.import(raw, proxy_id = nil)
    importer = new(raw, proxy_id)
    importer.import
    importer.report
  end

  # to be overriden in children
  def report_name_class
    raise NotImplementedError, "#{__method__} not implemented for this report importer"
  end

  def initialize(raw, proxy_id = nil)
    raise ::Foreman::Exception.new(_('Invalid report')) unless raw.is_a?(Hash) || raw.is_a?(ActionController::Parameters)
    @raw      = raw
    @proxy_id = proxy_id
  end

  def import
    logger.debug { "Processing report: #{raw.inspect}" }
    telemetry = {}
    telemetry_duration_histogram(:report_importer_create, :ms, {type: self.class.name}, telemetry) do
      create_report_and_logs
    end
    if report.persisted?
      telemetry_duration_histogram(:report_importer_refresh, :ms, {type: self.class.name}, telemetry) do
        host.refresh_statuses(statuses_for_refresh)
      end
      create = telemetry[:report_importer_create].try(:round, 1)
      refresh = telemetry[:report_importer_refresh].try(:round, 1)
      logger.info("Imported report for #{name} in #{create} ms, status refreshed in #{refresh} ms")
    end
  end

  def add_reporter_specific_data
    logger.info "Scanning report with: #{report_scanners.join(', ')}"
    report_scanners.each do |scanner|
      if (origin = scanner.identify_origin(raw))
        report.origin = origin
        scanner.add_reporter_data(report, raw)
        break
      end
    end
    logger.debug { "Changes after reporter specific data added: #{report.changes.inspect}" }
  end

  private

  attr_reader :raw, :proxy_id

  def name
    @name ||= raw['host']
  end

  def host
    hostname = name.downcase
    @host ||= Host::Base.find_by_certname(hostname) ||
      Host::Base.find_by_name(hostname) ||
      Host::Managed.new(:name => hostname)
  end

  def time
    @time ||= Time.parse(raw['reported_at']).utc
  end

  def logs
    raw['logs'] || []
  end

  def import_log_messages
    logs.each do |log|
      # Parse the API format
      level = log['log']['level']
      msg   = log['log']['messages']['message']
      src   = log['log']['sources']['source']

      message = Message.find_or_create_by(value: msg)
      source  = Source.find_or_create_by(value: src)

      # Symbols get turned into strings via the JSON API, so convert back here if it matches
      # and expected log level. Log objects can't be created without one, so raise if not
      raise(::Foreman::Exception.new(N_("Invalid log level: %s", level))) unless Report::LOG_LEVELS.include?(level)

      Log.create(:message_id => message.id, :source_id => source.id, :report => report, :level => level.to_sym)
    end
  end

  def report_status
    raise NotImplementedError
  end

  def statuses_for_refresh
    HostStatus.status_registry
  end

  def notify_on_report_error(mail_error_state)
    if report.error?
      # found a report with errors
      # notify via email IF enabled is set to true

      if host.disabled?
        logger.warn "#{name} is disabled - skipping alert"
        return
      end

      owners = host.owner.present? ? host.owner.recipients_for(:config_error_state) : []
      users = ConfigManagementError.all_hosts.flat_map(&:users)
      users = users.select do |user|
        User.as user do
          Host.authorized_as(user, :view_hosts).find(host.id).present?
        rescue ActiveRecord::RecordNotFound
          nil
        end
      end
      owners.concat users
      if owners.present?
        logger.debug { "sending alert to #{owners.map(&:login).join(',')}" }
        MailNotification[mail_error_state].deliver(report, :users => owners.uniq)
      else
        logger.debug { "no owner or recipients for alert on #{name}" }
      end
    end
  end

  def create_report_and_logs
    if host.new_record? && !Setting[:create_new_host_when_report_is_uploaded]
      logger.info("skipping report for #{name} as its an unknown host and create_new_host_when_report_is_uploaded setting is disabled")
      @report = report_name_class.new
      return @report
    end

    # we save the host without validation for two reasons:
    # 1. It might be auto imported, therefore might not be valid (e.g. missing partition table etc)
    # 2. We want this to be fast and light on the db.
    # at this point, the report is important, not the host
    host.save(:validate => false)

    status = report_status
    # and save our report
    @report = report_name_class.new(:host => host, :reported_at => time, :status => status, :metrics => raw['metrics'])

    # Run report scanner
    add_reporter_specific_data

    @report.save
    @report
  end

  def report_scanners
    Foreman::Plugin.report_scanner_registry.report_scanners
  end
end