Project

General

Profile

Download (12.3 KB) Statistics
| Branch: | Tag: | Revision:
87c40d2e Ohad Levy
class Report < ActiveRecord::Base
9fd7478e Paul Kelly
include Authorization
9390e9cf Ohad Levy
include ReportCommon
87c40d2e Ohad Levy
belongs_to :host
0de3b547 Ohad Levy
has_many :messages, :through => :logs
has_many :sources, :through => :logs
4cbaa406 Ohad Levy
has_many :logs, :dependent => :destroy
validates_presence_of :host_id, :reported_at, :status
9cb1dfc9 Ohad Levy
validates_uniqueness_of :reported_at, :scope => :host_id
87c40d2e Ohad Levy
b0b1ea21 Ohad Levy
scoped_search :in => :host, :on => :name, :complete_value => true, :rename => :host
scoped_search :in => :messages, :on => :value, :rename => :log
scoped_search :in => :sources, :on => :value, :rename => :resource

scoped_search :on => :reported_at, :complete_value => true, :default_order => :desc, :rename => :reported
a07977de Amos Benari
scoped_search :on => :status, :offset => 0, :word_size => 4*BIT_NUM, :complete_value => {:true => true, :false => false}, :rename => :eventful
b0b1ea21 Ohad Levy
scoped_search :on => :status, :offset => METRIC.index("applied"), :word_size => BIT_NUM, :rename => :applied
scoped_search :on => :status, :offset => METRIC.index("restarted"), :word_size => BIT_NUM, :rename => :restarted
scoped_search :on => :status, :offset => METRIC.index("failed"), :word_size => BIT_NUM, :rename => :failed
scoped_search :on => :status, :offset => METRIC.index("failed_restarts"), :word_size => BIT_NUM, :rename => :failed_restarts
scoped_search :on => :status, :offset => METRIC.index("skipped"), :word_size => BIT_NUM, :rename => :skipped
9b41cf08 Ohad Levy
scoped_search :on => :status, :offset => METRIC.index("pending"), :word_size => BIT_NUM, :rename => :pending
b0b1ea21 Ohad Levy
90ddcbb1 Greg Sutcliffe
# returns reports for hosts in the User's filter set
scope :my_reports, lambda {
c9579050 Greg Sutcliffe
return { :conditions => "" } if User.current.admin? # Admin can see all hosts

conditions = sanitize_sql_for_conditions([" (reports.host_id in (?))", Host.my_hosts.map(&:id)])
conditions.sub!(/\s*\(\)\s*/, "")
conditions.sub!(/^(?:\(\))?\s?(?:and|or)\s*/, "")
conditions.sub!(/\(\s*(?:or|and)\s*\(/, "((")
90ddcbb1 Greg Sutcliffe
{:conditions => conditions}
}

ff1cc6b1 Ohad Levy
# returns recent reports
84d8bcb2 Amos Benari
scope :recent, lambda { |*args| {:conditions => ["reported_at > ?", (args.first || 1.day.ago)], :order => "reported_at"} }
ff1cc6b1 Ohad Levy
16cb7742 Ohad Levy
# with_changes
017e1049 Ohad Levy
scope :interesting, {:conditions => "status != 0"}
ff1cc6b1 Ohad Levy
# a method that save the report values (e.g. values from METRIC)
# it is not supported to edit status values after it has been written once.
def status=(st)
s = st if st.is_a?(Integer)
s = Report.calc_status st if st.is_a?(Hash)
write_attribute(:status,s) unless s.nil?
87c40d2e Ohad Levy
end

4cbaa406 Ohad Levy
# extracts serialized metrics and keep them as a hash_with_indifferent_access
def metrics
YAML.load(read_attribute(:metrics)).with_indifferent_access
end

# serialize metrics as YAML
def metrics= m
write_attribute(:metrics,m.to_yaml) unless m.nil?
end

ff1cc6b1 Ohad Levy
def to_label
"#{host.name} / #{reported_at.to_s}"
9f9a8052 Ohad Levy
end

459e0feb Paul Kelly
def config_retrieval
50d977ed Ohad Levy
metrics[:time][:config_retrieval].round_with_precision(2) rescue 0
9f9a8052 Ohad Levy
end

def runtime
59be369d Amos Benari
(metrics[:time][:total] || metrics[:time].values.sum).round(2) rescue 0
9f9a8052 Ohad Levy
end

ff1cc6b1 Ohad Levy
#imports a YAML report into database
87c40d2e Ohad Levy
def self.import(yaml)
report = YAML.load(yaml)
90b70658 Ohad Levy
raise "Invalid report" unless report.is_a?(Puppet::Transaction::Report)
d7bb0ba7 Ohad Levy
logger.info "processing report for #{report.host}"
eafaf5f1 Ohad Levy
begin
dd42df0a Ohad Levy
host = Host.find_by_certname report.host
host ||= Host.find_by_name report.host
370a7ac7 Ohad Levy
host ||= Host.new :name => report.host
ff1cc6b1 Ohad Levy
# parse report metrics
370a7ac7 Ohad Levy
raise "Invalid report: can't find metrics information for #{host} at #{report.id}" if report.metrics.nil?
4cbaa406 Ohad Levy
# Is this a pre 2.6.x report format?
7123fa79 Ohad Levy
@post265 = report.instance_variables.include?("@report_format")
@pre26 = !report.instance_variables.include?("@resource_statuses")
4cbaa406 Ohad Levy
ff1cc6b1 Ohad Levy
# convert report status to bit field
16cb7742 Ohad Levy
st = calc_status(metrics_to_hash(report))
ff1cc6b1 Ohad Levy
# update host record
# we update our host record, so we wont need to lookup the report information just to display the host list / info
# save our report time
9cb1dfc9 Ohad Levy
host.last_report = report.time.utc if host.last_report.nil? or host.last_report.utc < report.time.utc
87c40d2e Ohad Levy
ff1cc6b1 Ohad Levy
# we save the raw bit status value in our host too.
host.puppet_status = st

9cb1dfc9 Ohad Levy
# we save the host without validation for two reasons:
ff1cc6b1 Ohad Levy
# 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.
9cb1dfc9 Ohad Levy
# at this point, the report is important, not as much of the host
017e1049 Ohad Levy
host.save(:validate => false)
9cb1dfc9 Ohad Levy
ff1cc6b1 Ohad Levy
# and save our report
4cbaa406 Ohad Levy
r = self.create!(:host => host, :reported_at => report.time.utc, :status => st, :metrics => self.m2h(report.metrics))
# Store all Puppet message logs
r.import_log_messages report
38dbbddf Ohad Levy
# if we are using storeconfigs then we already have the facts
# so we can refresh foreman internal fields accordingly
76607ed5 Ohad Levy
host.populateFieldsFromFacts if Setting[:using_storeconfigs]
2b174ff5 Eric Shamow
r.inspect_report
4cbaa406 Ohad Levy
return r
eafaf5f1 Ohad Levy
rescue Exception => e
459e0feb Paul Kelly
logger.warn "Failed to process report for #{report.host} due to:#{e}"
false
eafaf5f1 Ohad Levy
end
87c40d2e Ohad Levy
end

b2ef897d Ohad Levy
# returns a hash of hosts and their recent reports metric counts which have values
# e.g. non zero metrics.
# first argument is time range, everything afterwards is a host list.
# TODO: improve SQL query (so its not N+1 queries)
def self.summarise(time = 1.day.ago, *hosts)
ff1cc6b1 Ohad Levy
list = {}
b2ef897d Ohad Levy
raise "invalid host list" unless hosts
ff1cc6b1 Ohad Levy
hosts.flatten.each do |host|
b2ef897d Ohad Levy
# set default of 0 per metric
metrics = {}
ff1cc6b1 Ohad Levy
METRIC.each {|m| metrics[m] = 0 }
e6c75845 Ohad Levy
host.reports.recent(time).all(:select => "status").each do |r|
ff1cc6b1 Ohad Levy
metrics.each_key do |m|
b2ef897d Ohad Levy
metrics[m] += r.status(m)
ff1cc6b1 Ohad Levy
end
end
2dcf4736 Ohad Levy
list[host.name] = {:metrics => metrics, :id => host.id} if metrics.values.sum > 0
87c40d2e Ohad Levy
end
ff1cc6b1 Ohad Levy
return list
87c40d2e Ohad Levy
end

9cb1dfc9 Ohad Levy
# add sort by report time
def <=>(other)
self.created_at <=> other.created_at
end

56009410 Ohad Levy
# Expire reports based on time and status
# Defaults to expire reports older than a week regardless of the status
def self.expire(conditions = {})
timerange = conditions[:timerange] || 1.week
status = conditions[:status]
c820bdbb Ohad Levy
cond = "created_at < \'#{(Time.now.utc - timerange).to_formatted_s(:db)}\'"
56009410 Ohad Levy
cond += " and status = #{status}" unless status.nil?
0de3b547 Ohad Levy
# using find in batches to reduce the memory abuse
# trying to be smart about how to delete reports and their associated data, so it would be
# as fast as possible without a lot of performance penalties.
count = 0
Report.find_in_batches(:conditions => cond, :select => :id) do |reports|
report_ids = reports.map &:id
Log.delete_all({:report_id => report_ids})
count += Report.delete_all({:id => report_ids})
end
# try to find all non used logs, messages and sources

# first extract all information from our logs
all_reports, used_messages, used_sources = [],[],[]
Log.find_in_batches do |logs|
logs.each do |log|
all_reports << log.report_id unless log.report_id.blank?
used_messages << log.message_id unless log.message_id.blank?
used_sources << log.source_id unless log.source_id.blank?
end
end

all_reports.uniq! ; used_messages.uniq! ; used_sources.uniq!

# reports which have logs entries
used_reports = Report.all(:select => :id, :conditions => {:id => all_reports}).map(&:id)

orphaned_logs = all_reports - used_reports
Log.delete_all({:report_id => orphaned_logs}) unless orphaned_logs.empty?

all_messages = Message.all(:select => :id).map(&:id)
orphaned_messages = all_messages - used_messages
Message.delete_all({:id => orphaned_messages}) unless orphaned_messages.empty?

all_sources = Source.all(:select => :id).map(&:id)
orphaned_sources = all_sources - used_sources
Source.delete_all({:id => orphaned_sources}) unless orphaned_sources.empty?

56009410 Ohad Levy
logger.info Time.now.to_s + ": Expired #{count} Reports"
return count
87c40d2e Ohad Levy
end

4cbaa406 Ohad Levy
def import_log_messages report
report.logs.each do |r|
475455ed Ohad Levy
# skiping debug messages, we dont want them in our db
next if r.level == :debug
4cbaa406 Ohad Levy
message = Message.find_or_create_by_value r.message
source = Source.find_or_create_by_value r.source
log = Log.create :message_id => message.id, :source_id => source.id, :report_id => self.id, :level => r.level
log.errors.empty?
end
end

2b174ff5 Eric Shamow
def inspect_report
if error?
# found a report with errors
# notify via email IF enabled is set to true
logger.warn "#{report.host} is disabled - skipping." and return if host.disabled?

logger.debug "error detected, checking if we need to send an email alert"
017e1049 Ohad Levy
HostMailer.error_state(self).deliver if Setting[:failed_report_email_notification]
2b174ff5 Eric Shamow
# add here more actions - e.g. snmp alert etc
end
rescue => e
logger.warn "failed to send failure email notification: #{e}"
end

9390e9cf Ohad Levy
# represent if we have a report --> used to ensure consistency across host report state the report itself
def no_report
false
end

017e1049 Ohad Levy
def as_json(options={})
{:report =>
{ :reported_at => reported_at, :status => status,
:host => host.name, :metrics => metrics, :logs => logs.all(:include => [:source, :message]),
:id => id, :summary => summaryStatus
},
}
end
4cbaa406 Ohad Levy
private
90b70658 Ohad Levy
16cb7742 Ohad Levy
# Converts metrics form Puppet report into a hash
# this hash is required by the calc_status method
def self.metrics_to_hash(report)
report_status = {}
4cbaa406 Ohad Levy
metrics = report.metrics.with_indifferent_access
16cb7742 Ohad Levy
# find our metric values
4cbaa406 Ohad Levy
METRIC.each do |m|
if @pre26
27ecacf5 Ohad Levy
report_status[m] = metrics["resources"][m.to_sym]
4cbaa406 Ohad Levy
else
h=translate_metrics_to26(m)
7123fa79 Ohad Levy
mv = metrics[h[:type]]
report_status[m] = mv[h[:name].to_sym] + mv[h[:name].to_s] rescue nil
4cbaa406 Ohad Levy
end
report_status[m] ||= 0
end

16cb7742 Ohad Levy
# 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.sum) - report_status["skipped"] == report.logs.size)
report_status["skipped"] = 0
27ecacf5 Ohad Levy
end
7123fa79 Ohad Levy
# fix for reports that contain no metrics (i.e. failed catalog)
if @post265 and report.respond_to?(:status) and report.status == "failed"
report_status["failed"] += 1
end
16cb7742 Ohad Levy
return report_status
end

4cbaa406 Ohad Levy
# return all metrics as a hash
def self.m2h metrics
h = {}
metrics.each do |title, mtype|
h[mtype.name] ||= {}
mtype.values.each{|m| h[mtype.name].merge!({m[0] => m[2]})}
end
return h
end


16cb7742 Ohad Levy
# converts a hash into a bit field
# expects a metrics_to_hash kind of hash
ff1cc6b1 Ohad Levy
def self.calc_status (hash = {})
st = 0
hash.each do |type, value|
value = MAX if value > MAX # we store up to 2^BIT_NUM -1 values as we want to use only BIT_NUM bits.
st |= value << (BIT_NUM*METRIC.index(type))
end
return st
90b70658 Ohad Levy
end

efc301c5 Ohad Levy
def validate_meteric (type, name)
45dad022 Ohad Levy
log.metrics[type][name].to_f
rescue Exception => e
logger.warn "failed to process report due to #{e}"
nil
efc301c5 Ohad Levy
end

4cbaa406 Ohad Levy
# 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 self.translate_metrics_to26 metric
9fd7478e Paul Kelly
case metric
4cbaa406 Ohad Levy
when "applied"
7123fa79 Ohad Levy
if @post265
{ :type => "changes", :name => "total"}
else
{ :type => "total", :name => :changes}
end
2a1616ed Tim Speetjens
when "failed_restarts"
if @pre26
{ :type => "resources", :name => metric}
else
{ :type => "resources", :name => "failed_to_restart"}
end
9b41cf08 Ohad Levy
when "pending"
{ :type => "events", :name => "noop" }
4cbaa406 Ohad Levy
else
7123fa79 Ohad Levy
{ :type => "resources", :name => metric}
4cbaa406 Ohad Levy
end
end

9fd7478e Paul Kelly
def enforce_permissions operation
# No one can edit a report
return false if operation == "edit"

# Anyone can create a report
return true if operation == "create"
return true if operation == "destroy" and User.current.allowed_to?(:destroy_reports)

017e1049 Ohad Levy
errors.add :base, "You do not have permission to #{operation} this report"
9fd7478e Paul Kelly
false
end
f3c1ecd3 Ohad Levy
778b5a0a Ohad Levy
def summaryStatus
return "Failed" if error?
return "Modified" if changes?
return "Success"
925b276b Corey Osman
end

9390e9cf Ohad Levy
# puppet report status table column name
def self.report_status
"status"
end
87c40d2e Ohad Levy
end