Class: Report

Inherits:
ActiveRecord::Base
  • Object
show all
Includes:
Authorization, ReportCommon
Defined in:
app/models/report.rb

Constant Summary

Constants included from ReportCommon

ReportCommon::BIT_NUM, ReportCommon::MAX, ReportCommon::METRIC

Class Method Summary collapse

Instance Method Summary collapse

Methods included from ReportCommon

#changes?, #error?, included, #pending?, #status

Methods included from Authorization

#enforce_create_permissions, #enforce_destroy_permissions, #enforce_edit_permissions, included, #permission_failed?

Class Method Details

.expire(conditions = {}) ⇒ Object

Expire reports based on time and status Defaults to expire reports older than a week regardless of the status


159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
# File 'app/models/report.rb', line 159

def self.expire(conditions = {})
  timerange = conditions[:timerange] || 1.week
  status = conditions[:status]
  cond = "created_at < \'#{(Time.now.utc - timerange).to_formatted_s(:db)}\'"
  cond += " and status = #{status}" unless status.nil?
  # 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?

  logger.info Time.now.to_s + ": Expired #{count} Reports"
  return count
end

.import(yaml) ⇒ Object

imports a YAML report into database


76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
# File 'app/models/report.rb', line 76

def self.import(yaml)
  report = YAML.load(yaml)
  raise ::Foreman::Exception.new(N_("Invalid report")) unless report.is_a?(Puppet::Transaction::Report)
  logger.info "processing report for #{report.host}"
  begin
    host = Host.find_by_certname report.host
    host ||= Host.find_by_name report.host
    host ||= Host.new :name => report.host

    # parse report metrics
    raise "Invalid report: can't find metrics information for #{host} at #{report.id}" if report.metrics.nil?

    case
    when report.instance_variables.detect {|v| v.to_s == "@environment"}
      @format = 3
    when report.instance_variables.detect {|v| v.to_s == "@report_format"}
      @format = 2
    when report.instance_variables.detect {|v| v.to_s == "@resource_statuses"}
      @format = 1
    else
      @format = 0
    end

    # convert report status to bit field
    st = calc_status(metrics_to_hash(report))

    # 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
    host.last_report = report.time.utc if host.last_report.nil? or host.last_report.utc < report.time.utc

    # we save the raw bit status value in our host too.
    host.puppet_status = st

    # 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 as much of the host
    host.save(:validate => false)

    # and save our report
    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
    # if we are using storeconfigs then we already have the facts
    # so we can refresh foreman internal fields accordingly
    host.populateFieldsFromFacts if Setting[:using_storeconfigs] == true
    r.inspect_report
    return r
  rescue Exception => e
    logger.warn "Failed to process report for #{report.host} due to:#{e}"
    false
  end
end

.summarise(time = 1.day.ago, *hosts) ⇒ Object

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)


135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
# File 'app/models/report.rb', line 135

def self.summarise(time = 1.day.ago, *hosts)
  list = {}
  raise ::Foreman::Exception.new(N_("invalid host list")) unless hosts
  hosts.flatten.each do |host|
    # set default of 0 per metric
    metrics = {}
    METRIC.each {|m| metrics[m] = 0 }
    host.reports.recent(time).all(:select => "status").each do |r|
      metrics.each_key do |m|
        metrics[m] += r.status(m)
      end
    end
    list[host.name] = {:metrics => metrics, :id => host.id} if metrics.values.sum > 0
  end
  return list
end

Instance Method Details

#<=>(other) ⇒ Object

add sort by report time


153
154
155
# File 'app/models/report.rb', line 153

def <=>(other)
  self.created_at <=> other.created_at
end

#as_json(options = {}) ⇒ Object


237
238
239
240
241
242
243
244
# File 'app/models/report.rb', line 237

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

#config_retrievalObject


67
68
69
# File 'app/models/report.rb', line 67

def config_retrieval
  metrics[:time][:config_retrieval].round(2) rescue 0
end

#import_log_messages(report) ⇒ Object


205
206
207
208
209
210
211
212
213
214
215
216
# File 'app/models/report.rb', line 205

def import_log_messages report
  report.logs.each do |r|
    # skipping debug messages, we dont want them in our db
    next if r.level == :debug
    # skipping catalog summary run messages, we dont want them in our db too
    next if r.message =~ /^Finished catalog run in \d+.\d+ seconds$/
    message = Message.find_or_create r.message
    source  = Source.find_or_create 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

#inspect_reportObject


218
219
220
221
222
223
224
225
226
227
228
229
230
# File 'app/models/report.rb', line 218

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"
    HostMailer.error_state(self).deliver if Setting[:failed_report_email_notification]
    # add here more actions - e.g. snmp alert etc
  end
rescue => e
  logger.warn "failed to send failure email notification: #{e}"
end

#metricsObject

extracts serialized metrics and keep them as a hash_with_indifferent_access


54
55
56
# File 'app/models/report.rb', line 54

def metrics
  YAML.load(read_attribute(:metrics)).with_indifferent_access
end

#metrics=(m) ⇒ Object

serialize metrics as YAML


59
60
61
# File 'app/models/report.rb', line 59

def metrics= m
  write_attribute(:metrics,m.to_yaml) unless m.nil?
end

#no_reportObject

represent if we have a report –> used to ensure consistency across host report state the report itself


233
234
235
# File 'app/models/report.rb', line 233

def no_report
  false
end

#runtimeObject


71
72
73
# File 'app/models/report.rb', line 71

def runtime
  (metrics[:time][:total] || metrics[:time].values.sum).round(2) rescue 0
end

#status=(st) ⇒ Object

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.


47
48
49
50
51
# File 'app/models/report.rb', line 47

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?
end

#summaryStatusObject


246
247
248
249
250
# File 'app/models/report.rb', line 246

def summaryStatus
  return _("Failed")   if error?
  return _("Modified") if changes?
  return _("Success")
end

#to_labelObject


63
64
65
# File 'app/models/report.rb', line 63

def to_label
  "#{host.name} / #{reported_at.to_s}"
end