foremansalt

Created Diff never expires
53 removals
Words removed124
Total words481
Words removed (%)25.78
176 lines
143 additions
Words added441
Total words798
Words added (%)55.26
262 lines
module ForemanSalt
module ForemanSalt
class ReportImporter
class ReportImporter
delegate :logger, to: :Rails
delegate :logger, to: :Rails
attr_reader :report
attr_reader :report


# Define logger as a class method
def self.logger
Rails.logger
end

def self.import(raw, proxy_id = nil)
def self.import(raw, proxy_id = nil)
logger.info "Starting import with raw data: #{raw.inspect} and proxy_id: #{proxy_id}"
raise ::Foreman::Exception, _('Invalid report') unless raw.is_a?(Hash)
raise ::Foreman::Exception, _('Invalid report') unless raw.is_a?(Hash)


raw.map do |host, report|
raw.map do |host, report|
logger.debug "Processing report for host: #{host}"
importer = ForemanSalt::ReportImporter.new(host, report, proxy_id)
importer = ForemanSalt::ReportImporter.new(host, report, proxy_id)
importer.import
importer.import
report = importer.report
report = importer.report
report.origin = 'Salt'
report.origin = 'Salt'
report.save!
report.save!
logger.info "Report saved for host: #{host}"
report
report
end
end
rescue => e
logger.error "Import failed: #{e.message}"
raise e
end
end


def initialize(host, raw, proxy_id = nil)
def initialize(host, raw, proxy_id = nil)
logger.debug "Initializing ReportImporter with host: #{host}, proxy_id: #{proxy_id}"
@host = find_or_create_host(host)
@host = find_or_create_host(host)
@raw = raw
@raw = raw
@proxy_id = proxy_id
@proxy_id = proxy_id
end
end


def import
def import
logger.info "processing report for #{@host}"
logger.info "Processing report for #{@host}"
logger.debug { "Report: #{@raw.inspect}" }
logger.debug { "Report raw data: #{@raw.inspect}" }


if @host.new_record? && !Setting[:create_new_host_when_report_is_uploaded]
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")
logger.info("Skipping report for #{@host} as it's an unknown host and create_new_host_when_report_is_uploaded setting is disabled")
return ConfigReport.new
return ConfigReport.new
end
end


@host.salt_proxy_id ||= @proxy_id
@host.salt_proxy_id ||= @proxy_id
@host.last_report = start_time
@host.last_report = start_time


if [Array, String].member? @raw.class
if [Array, String].include?(@raw.class)
logger.debug "Detected failures in raw data; processing failures"
process_failures # If Salt sends us only an array (or string), it's a list of fatal failures
process_failures # If Salt sends us only an array (or string), it's a list of fatal failures
else
else
logger.debug "Processing normal report data"
process_normal
process_normal
end
end


@host.save(validate: false)
if @host.save(validate: true)
logger.debug "Host #{@host.name} saved successfully"
else
logger.warn "Host #{@host.name} failed to save: #{@host.errors.full_messages.join(', ')}"
end

@host.reload
@host.reload
@host.refresh_statuses([HostStatus.find_status_by_humanized_name('configuration')])
@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")
duration = (Time.zone.now - start_time).round(2)
logger.info("Imported report for #{@host} in #{duration} seconds")
rescue => e
logger.error "Failed to import report for #{@host}: #{e.message}"
raise e
end
end


private
private


def find_or_create_host(host)
def find_or_create_host(host)
logger.debug "Finding or creating host: #{host}"
@host ||= Host::Managed.find_by(name: host)
@host ||= Host::Managed.find_by(name: host)


unless @host
unless @host
new = Host::Managed.new(name: host)
logger.info "Host not found; creating new host with name: #{host}"
new.save(validate: false)
new_host = Host::Managed.new(name: host)
@host = new
if new_host.save(validate: true)
logger.debug "New host #{host} created successfully"
@host = new_host
else
logger.error "Failed to create host #{host}: #{new_host.errors.full_messages.join(', ')}"
raise ::Foreman::Exception, _('Failed to create host')
end
end
end


@host
@host
rescue => e
logger.error "Error in find_or_create_host: #{e.message}"
raise e
end
end


def import_log_messages
def import_log_messages
logger.debug "Importing log messages"
@raw.each do |resource, result|
@raw.each do |resource, result|
level = if result['changes'].blank? && result['result']
level = determine_log_level(result)
:info
source_value = resource.to_s
elsif result['result'] == false
source = Source.find_or_create_by(value: source_value)
:err
logger.debug "Log source: #{source_value}"
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']
message_value = extract_message(result)
result['changes']['diff']
message = Message.find_or_create_by(value: message_value)
elsif result['pchanges'].present? && result['pchanges'].include?('diff')
logger.debug "Log message: #{message_value}"
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)
Log.create(message_id: message.id, source_id: source.id, report: @report, level: level)
logger.debug "Log entry created with level #{level} for resource #{resource}"
end
end
rescue => e
logger.error "Error importing log messages: #{e.message}"
raise e
end
end


def calculate_metrics
def calculate_metrics
logger.debug "Calculating metrics from raw data"
success = 0
success = 0
failed = 0
failed = 0
changed = 0
changed = 0
restarted = 0
restarted = 0
restarted_failed = 0
restarted_failed = 0
pending = 0
pending = 0


time = {}
time = {}


@raw.each do |resource, result|
@raw.each do |resource, result|
next unless result.is_a? Hash
next unless result.is_a?(Hash)
logger.debug "Processing resource: #{resource}"


if result['result']
if result['result'] == true
success += 1
success += 1
logger.debug "Resource #{resource} succeeded"
if resource.match(/^service_/) && result['comment'].include?('restarted')
if resource.match(/^service_/) && result['comment'].include?('restarted')
restarted += 1
restarted += 1
logger.debug "Service #{resource} was restarted"
elsif result['changes'].present?
elsif result['changes'].present?
changed += 1
changed += 1
logger.debug "Resource #{resource} changed"
elsif result['pchanges'].present?
elsif result['pchanges'].present?
pending += 1
pending += 1
logger.debug "Resource #{resource} has pending changes"
end
end
elsif result['result'].nil?
elsif result['result'].nil?
pending += 1
pending += 1
elsif !result['result']
logger.debug "Resource #{resource} is pending"
elsif result['result'] == false
if resource.match(/^service_/) && result['comment'].include?('restarted')
if resource.match(/^service_/) && result['comment'].include?('restarted')
restarted_failed += 1
restarted_failed += 1
logger.debug "Service #{resource} failed to restart"
else
else
failed += 1
failed += 1
logger.debug "Resource #{resource} failed"
end
end
end
end


duration = if result['duration'].is_a? String
duration = parse_duration(result['duration'])
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
time[resource] = duration || 0
logger.debug "Duration for resource #{resource}: #{time[resource]} seconds"
end
end


time[:total] = time.values.compact.sum || 0
time[:total] = time.values.compact.sum || 0
logger.debug "Total execution time: #{time[:total]} seconds"

events = { total: changed + failed + restarted + restarted_failed, success: success + restarted, failure: failed + restarted_failed }
events = { total: changed + failed + restarted + restarted_failed, success: success + restarted, failure: failed + restarted_failed }

changes = { total: changed + restarted }
changes = { total: changed + restarted }
resources = {
'total' => @raw.size,
'applied' => changed,
'restarted' => restarted,
'failed' => failed,
'failed_restarts' => restarted_failed,
'skipped' => 0,
'scheduled' => 0,
'pending' => pending
}


resources = { 'total' => @raw.size, 'applied' => changed, 'restarted' => restarted, 'failed' => failed,
logger.debug "Metrics calculated: events=#{events}, changes=#{changes}, resources=#{resources}"
'failed_restarts' => restarted_failed, 'skipped' => 0, 'scheduled' => 0, 'pending' => pending }


{ events: events, resources: resources, changes: changes, time: time }
{ events: events, resources: resources, changes: changes, time: time }
rescue => e
logger.error "Error calculating metrics: #{e.message}"
raise e
end
end


def process_normal
def process_normal
logger.debug "Processing normal report"
metrics = calculate_metrics
metrics = calculate_metrics
status = ConfigReportStatusCalculator.new(counters: metrics[:resources].slice(*::ConfigReport::METRIC)).calculate
status = ConfigReportStatusCalculator.new(counters: metrics[:resources].slice(*::ConfigReport::METRIC)).calculate
logger.debug "Calculated status: #{status.inspect}"


@report = ConfigReport.new(host: @host, reported_at: start_time, status: status, metrics: metrics)
@report = ConfigReport.new(host: @host, reported_at: start_time, status: status, metrics: metrics)
return @report unless @report.save
if @report.save

logger.debug "Report saved successfully for host #{@host.name}"
import_log_messages
import_log_messages
else
logger.error "Failed to save report: #{@report.errors.full_messages.join(', ')}"
end
rescue => e
logger.error "Error processing normal report: #{e.message}"
raise e
end
end


def process_failures
def process_failures
@raw = [@raw] unless @raw.is_a? Array
logger.debug "Processing failure report"
@raw = [@raw] unless @raw.is_a?(Array)
status = ConfigReportStatusCalculator.new(counters: { 'failed' => @raw.size }).calculate
status = ConfigReportStatusCalculator.new(counters: { 'failed' => @raw.size }).calculate
logger.debug "Calculated status for failures: #{status.inspect}"

@report = ConfigReport.create(host: @host, reported_at: Time.zone.now, status: status, metrics: {})
@report = ConfigReport.create(host: @host, reported_at: Time.zone.now, status: status, metrics: {})


source = Source.find_or_create_by(value: 'Salt')
source = Source.find_or_create_by(value: 'Salt')
@raw.each do |failure|
@raw.each do |failure|
message = Message.find_or_create_by(value: failure)
message = Message.find_or_create_by(value: failure)
Log.create(message_id: message.id, source_id: source.id, report: @report, level: :err)
Log.create(message_id: message.id, source_id: source.id, report: @report, level: :err)
logger.debug "Logged failure message: #{failure}"
end
end
rescue => e
logger.error "Error processing failures: #{e.message}"
raise e
end
end


def start_time
def start_time
@start_time ||= Time.zone.now
@start_time ||= Time.zone.now
end
end

def determine_log_level(result)
if result['changes'].blank? && result['result'] == true
:info
elsif result['result'] == false
:err
else
:notice
end
end

def extract_message(result)
if result['changes'] && result['changes']['diff']
result['changes']['diff']
elsif result['pchanges'] && result['pchanges']['diff']
result['pchanges']['diff']
elsif result['comment'].present?
result['comment']
else
'No message available'
end
end

def parse_duration(duration)
if duration.is_a?(String)
duration_in_ms = duration.delete(' ms')
Float(duration_in_ms) / 1000
else
duration.to_f / 1000
end
rescue
logger.warn "Unable to parse duration: #{duration}"
nil
end
end
end
end
end