foreman/app/models/report.rb @ 84d8bcb2
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
|