Project

General

Profile

« Previous | Next » 

Revision e8d6d2d6

Added by Shlomi Zadok over 8 years ago

fixes #4151 - enable reports STI

Permits subclassing of ReportImporter and Report to import and store
new types of reports associated to hosts.

View differences:

app/controllers/api/v1/reports_controller.rb
module Api
module V1
class ReportsController < V1::BaseController
before_filter :deprecated
before_filter :find_resource, :only => %w{show destroy}
before_filter :setup_search_options, :only => [:index, :last]
......
param :per_page, String, :desc => "number of entries per request"
def index
@reports = Report.
authorized(:view_reports).
@reports = ConfigReport.
authorized(:view_config_reports).
my_reports.
includes(:logs => [:source, :message]).
search_for(*search_options).paginate(paginate_options)
......
private
def deprecated
Foreman::Deprecation.api_deprecation_warning("Reports were renamed to ConfigReports")
end
def resource_scope(options = {})
options.merge!(:permission => :view_config_reports)
super(options).my_reports
end
def resource_class
ConfigReport
end
def action_permission
case params[:action]
when 'last'
app/controllers/api/v2/config_reports_controller.rb
module Api
module V2
class ConfigReportsController < V2::BaseController
include Api::Version2
include Foreman::Controller::SmartProxyAuth
before_filter :find_resource, :only => %w{show destroy}
before_filter :setup_search_options, :only => [:index, :last]
add_smart_proxy_filters :create, :features => Proc.new { ConfigReportImporter.authorized_smart_proxy_features }
api :GET, "/config_reports/", N_("List all reports")
param_group :search_and_pagination, ::Api::V2::BaseController
def index
@config_reports = resource_scope_for_index.my_reports.includes(:logs => [:source, :message])
@total = ConfigReport.my_reports.count
end
api :GET, "/config_reports/:id/", N_("Show a report")
param :id, :identifier, :required => true
def show
end
def_param_group :config_report do
param :config_report, Hash, :required => true, :action_aware => true do
param :host, String, :required => true, :desc => N_("Hostname or certname")
param :reported_at, String, :required => true, :desc => N_("UTC time of report")
param :status, Hash, :required => true, :desc => N_("Hash of status type totals")
param :metrics, Hash, :required => true, :desc => N_("Hash of report metrics, can be just {}")
param :logs, Array, :desc => N_("Optional array of log hashes")
end
end
api :POST, "/config_reports/", N_("Create a report")
param_group :config_report, :as => :create
def create
@config_report = ConfigReport.import(params[:config_report], detected_proxy.try(:id))
process_response @config_report.errors.empty?
rescue ::Foreman::Exception => e
render_message(e.to_s, :status => :unprocessable_entity)
end
api :DELETE, "/config_reports/:id/", N_("Delete a report")
param :id, String, :required => true
def destroy
process_response @config_report.destroy
end
api :GET, "/hosts/:host_id/config_reports/last", N_("Show the last report for a host")
param :id, :identifier, :required => true
def last
conditions = { :host_id => Host.authorized(:view_hosts).find(params[:host_id]).try(:id) } if params[:host_id].present?
max_id = resource_scope.where(conditions).maximum(:id)
@config_report = resource_scope.includes(:logs => [:message, :source]).find(max_id)
render :show
end
private
def resource_scope(options = {})
options.merge!(:permission => :view_config_reports)
super(options).my_reports
end
def action_permission
case params[:action]
when 'last'
'view'
else
super
end
end
end
end
end
app/controllers/api/v2/reports_controller.rb
class ReportsController < V2::BaseController
include Api::Version2
include Foreman::Controller::SmartProxyAuth
before_filter :deprecated
before_filter :find_resource, :only => %w{show destroy}
before_filter :setup_search_options, :only => [:index, :last]
add_smart_proxy_filters :create, :features => Proc.new { ReportImporter.report_features }
add_smart_proxy_filters :create, :features => Proc.new { ConfigReportImporter.authorized_smart_proxy_features }
api :GET, "/reports/", N_("List all reports")
param_group :search_and_pagination, ::Api::V2::BaseController
def index
@reports = resource_scope_for_index.my_reports.includes(:logs => [:source, :message])
@total = Report.my_reports.count
@total = resource_class.my_reports.count
end
api :GET, "/reports/:id/", N_("Show a report")
......
param_group :report, :as => :create
def create
@report = Report.import(params[:report], detected_proxy.try(:id))
@report = resource_class.import(params[:report], detected_proxy.try(:id))
process_response @report.errors.empty?
rescue ::Foreman::Exception => e
render_message(e.to_s, :status => :unprocessable_entity)
......
private
def deprecated
Foreman::Deprecation.api_deprecation_warning("The resources /reports were moved to /config_reports. Please use the new path instead")
end
def resource_class
ConfigReport
end
def resource_scope(options = {})
super(options).my_reports
super(options.merge(:permission => :view_config_reports)).my_reports
end
def action_permission
app/controllers/config_reports_controller.rb
class ConfigReportsController < ApplicationController
include Foreman::Controller::AutoCompleteSearch
before_filter :setup_search_options, :only => :index
def index
@config_reports = resource_base.search_for(params[:search], :order => params[:order]).paginate(:page => params[:page], :per_page => params[:per_page]).includes(:host)
end
def show
# are we searching for the last report?
if params[:id] == "last"
conditions = { :host_id => Host.authorized(:view_hosts).find(params[:host_id]).try(:id) } if params[:host_id].present?
params[:id] = resource_base.where(conditions).maximum(:id)
end
return not_found if params[:id].blank?
@config_report = resource_base.includes(:logs => [:message, :source]).find(params[:id])
@offset = @config_report.reported_at - @config_report.created_at
end
def destroy
@config_report = resource_base.find(params[:id])
if @config_report.destroy
process_success(:success_msg => _("Successfully deleted report."), :success_redirect => config_reports_path)
else
process_error
end
end
private
def resource_base
super.my_reports
end
end
app/controllers/hosts_controller.rb
format.html do
@hosts = search.includes(included_associations).paginate(:page => params[:page])
# SQL optimizations queries
@last_report_ids = Report.where(:host_id => @hosts.map(&:id)).group(:host_id).maximum(:id)
@last_reports = Report.where(:id => @last_report_ids.values)
@last_report_ids = ConfigReport.where(:host_id => @hosts.map(&:id)).group(:host_id).maximum(:id)
@last_reports = ConfigReport.where(:id => @last_report_ids.values)
# rendering index page for non index page requests (out of sync hosts etc)
@hostgroup_authorizer = Authorizer.new(User.current, :collection => @hosts.map(&:hostgroup_id).compact.uniq)
render :index if title and (@title = title)
......
@range = (params["range"].empty? ? 7 : params["range"].to_i)
# summary report text
@report_summary = Report.summarise(@range.days.ago, @host)
@report_summary = ConfigReport.summarise(@range.days.ago, @host)
end
format.yaml { render :text => @host.info.to_yaml }
format.json
app/controllers/reports_controller.rb
class ReportsController < ApplicationController
include Foreman::Controller::AutoCompleteSearch
before_filter :setup_search_options, :only => :index
def index
@reports = resource_base.search_for(params[:search], :order => params[:order]).paginate(:page => params[:page], :per_page => params[:per_page]).includes(:host)
end
def show
# are we searching for the last report?
if params[:id] == "last"
conditions = { :host_id => Host.authorized(:view_hosts).find(params[:host_id]).try(:id) } if params[:host_id].present?
params[:id] = resource_base.where(conditions).maximum(:id)
end
@report = resource_base.includes(:logs => [:message, :source]).find(params[:id])
@offset = @report.reported_at - @report.created_at
end
def destroy
@report = resource_base.find(params[:id])
if @report.destroy
process_success(:success_msg => _("Successfully deleted report."), :success_redirect => reports_path)
else
process_error
end
end
private
def resource_base
super.my_reports
end
end
app/helpers/dashboard_helper.rb
def latest_events
# 6 reports + header fits the events box nicely...
Report.authorized(:view_reports).my_reports.interesting.search_for('reported > "7 days ago"').limit(6).includes(:host)
ConfigReport.authorized(:view_config_reports).my_reports.interesting.search_for('reported > "7 days ago"').limit(6).includes(:host)
end
def translated_header(shortname, longname)
app/helpers/hosts_helper.rb
def last_report_column(record)
time = record.last_report? ? _("%s ago") % time_ago_in_words(record.last_report): ""
link_to_if_authorized(time,
hash_for_host_report_path(:host_id => record.to_param, :id => "last"),
hash_for_host_config_report_path(:host_id => record.to_param, :id => "last"),
last_report_tooltip(record))
end
......
def show_appropriate_host_buttons(host)
[ link_to_if_authorized(_("Audits"), hash_for_host_audits_path(:host_id => @host), :title => _("Host audit entries"), :class => 'btn btn-default'),
(link_to_if_authorized(_("Facts"), hash_for_host_facts_path(:host_id => host), :title => _("Browse host facts"), :class => 'btn btn-default') if host.fact_values.any?),
(link_to_if_authorized(_("Reports"), hash_for_host_reports_path(:host_id => host), :title => _("Browse host reports"), :class => 'btn btn-default') if host.reports.any?),
(link_to_if_authorized(_("Reports"), hash_for_host_config_reports_path(:host_id => host), :title => _("Browse host config management reports"), :class => 'btn btn-default') if host.reports.any?),
(link_to(_("YAML"), externalNodes_host_path(:name => host), :title => _("Puppet external nodes YAML dump"), :class => 'btn btn-default') if SmartProxy.with_features("Puppet").any?)
].compact
end
app/helpers/reports_helper.rb
require 'ostruct'
module ReportsHelper
def reported_at_column(record)
link_to(_("%s ago") % time_ago_in_words(record.reported_at), report_path(record))
link_to(_("%s ago") % time_ago_in_words(record.reported_at), config_report_path(record))
end
def report_event_column(event, style = "")
......
end
def logs_show
return unless @report.logs.size > 0
form_tag @report, :id => 'level_filter', :method => :get, :class => "form form-horizontal" do
return unless @config_report.logs.size > 0
form_tag config_report_path(@config_report), :id => 'level_filter', :method => :get, :class => "form form-horizontal" do
content_tag(:span, _("Show log messages:") + ' ') +
select(nil, 'level', [[_('All messages'), 'info'],[_('Notices, warnings and errors'), 'notice'],[_('Warnings and errors'), 'warning'],[_('Errors only'), 'error']],
{}, {:class=>"col-md-1 form-control", :onchange =>"filter_by_level(this);"})
app/mailers/host_mailer.rb
raise ::Foreman::Exception.new(N_("Must specify a valid user with email enabled")) unless (user=User.find(options[:user]))
hosts = Host::Managed.authorized_as(user, :view_hosts, Host)
time = options[:time] || 1.day.ago
host_data = Report.summarise(time, hosts.all).sort
host_data = ConfigReport.summarise(time, hosts.all).sort
total_metrics = load_metrics(host_data)
total = 0
app/models/concerns/configuration_status_scoped_search.rb
module ClassMethods
def scoped_search_status(status, options)
options.merge!({ :offset => Report::METRIC.index(status.to_s), :word_size => Report::BIT_NUM })
options.merge!({ :offset => ConfigReport::METRIC.index(status.to_s), :word_size => ConfigReport::BIT_NUM })
scoped_search options
end
end
app/models/concerns/hostext/search.rb
scoped_search :on => :owner_type, :complete_value => true, :only_explicit => true
scoped_search :on => :owner_id, :complete_enabled => false, :only_explicit => true
scoped_search :in => :configuration_status_object, :on => :status, :offset => 0, :word_size => Report::BIT_NUM*4, :rename => :'status.interesting', :complete_value => {:true => true, :false => false}
scoped_search :in => :configuration_status_object, :on => :status, :offset => 0, :word_size => ConfigReport::BIT_NUM*4, :rename => :'status.interesting', :complete_value => {:true => true, :false => false}
scoped_search_status "applied", :in => :configuration_status_object, :on => :status, :rename => :'status.applied'
scoped_search_status "restarted", :in => :configuration_status_object, :on => :status, :rename => :'status.restarted'
scoped_search_status "failed", :in => :configuration_status_object, :on => :status, :rename => :'status.failed'
app/models/config_report.rb
class ConfigReport < Report
METRIC = %w[applied restarted failed failed_restarts skipped pending]
BIT_NUM = 6
MAX = (1 << BIT_NUM) -1 # maximum value per metric
scoped_search :on => :status, :offset => 0, :word_size => 4*BIT_NUM, :complete_value => {:true => true, :false => false}, :rename => :eventful
scoped_search_status 'applied', :on => :status, :rename => :applied
scoped_search_status 'restarted', :on => :status, :rename => :restarted
scoped_search_status 'failed', :on => :status, :rename => :failed
scoped_search_status 'failed_restarts', :on => :status, :rename => :failed_restarts
scoped_search_status 'skipped', :on => :status, :rename => :skipped
scoped_search_status 'pending', :on => :status, :rename => :pending
# search for a metric - e.g.:
# Report.with("failed") --> all reports which have a failed counter > 0
# Report.with("failed",20) --> all reports which have a failed counter > 20
scope :with, lambda { |*arg|
where("(#{report_status_column} >> #{HostStatus::ConfigurationStatus.bit_mask(arg[0].to_s)}) > #{arg[1] || 0}")
}
class << self
delegate :model_name, :to => :superclass
end
def self.import(report, proxy_id = nil)
ConfigReportImporter.import(report, proxy_id)
end
# puppet report status table column name
def self.report_status_column
"status"
end
# 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 = case st
when Integer, Fixnum
st
when Hash
ConfigReportStatusCalculator.new(:counters => st).calculate
else
raise Foreman::Exception(N_('Unsupported report status format'))
end
write_attribute(:status, s)
end
def config_retrieval
metrics[:time][:config_retrieval].round(2) rescue 0
end
def runtime
(metrics[:time][:total] || metrics[:time].values.sum).round(2) rescue 0
end
# 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)
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).select(:status).each do |r|
metrics.each_key do |m|
metrics[m] += r.status_of(m)
end
end
list[host.name] = {:metrics => metrics, :id => host.id} if metrics.values.sum > 0
end
list
end
def summary_status
return _("Failed") if error?
return _("Modified") if changes?
_("Success")
end
delegate :error?, :changes?, :pending?, :status, :status_of, :to => :calculator
delegate(*METRIC, :to => :calculator)
def calculator
ConfigReportStatusCalculator.new(:bit_field => read_attribute(self.class.report_status_column))
end
end
app/models/host/managed.rb
has_many :host_classes, :foreign_key => :host_id
has_many :puppetclasses, :through => :host_classes, :dependent => :destroy
belongs_to :hostgroup
has_many :reports, :foreign_key => :host_id
has_one :last_report_object, :foreign_key => :host_id, :order => "#{Report.table_name}.id DESC", :class_name => 'Report'
has_many :reports, :foreign_key => :host_id, :class_name => 'ConfigReport'
has_one :last_report_object, :foreign_key => :host_id, :order => "#{Report.table_name}.id DESC", :class_name => 'ConfigReport'
has_many :host_parameters, :dependent => :destroy, :foreign_key => :reference_id, :inverse_of => :host
has_many :parameters, :dependent => :destroy, :foreign_key => :reference_id, :class_name => "HostParameter"
accepts_nested_attributes_for :host_parameters, :allow_destroy => true
......
self.owner = oid
end
def clearReports
def clear_reports
# Remove any reports that may be held against this host
Report.where("host_id = #{id}").delete_all
end
def clearFacts
def clear_facts
FactValue.where("host_id = #{id}").delete_all
end
def clear_data_on_build
return unless respond_to?(:old) && old && build? && !old.build?
clearFacts
clearReports
clear_facts
clear_reports
end
def set_token
......
end
def puppet_status
Foreman::Deprecation.deprecation_warning('1.12', 'Host#puppet_status has been deprecated, you should use configuration_status')
Foreman::Deprecation.deprecation_warning('1.13', 'Host#puppet_status has been deprecated, you should use configuration_status')
configuration_status
end
app/models/host_status/configuration_status.rb
module HostStatus
class ConfigurationStatus < Status
delegate :error?, :changes?, :pending?, :to => :calculator
delegate(*Report::METRIC, :to => :calculator)
delegate(*ConfigReport::METRIC, :to => :calculator)
def last_report
self.last_report = host.last_report_object unless @last_report_set
......
end
def self.bit_mask(config_status)
"#{Report::BIT_NUM * Report::METRIC.index(config_status)} & #{Report::MAX}"
"#{ConfigReport::BIT_NUM * ConfigReport::METRIC.index(config_status)} & #{ConfigReport::MAX}"
end
private
......
end
def calculator
ReportStatusCalculator.new(:bit_field => status)
ConfigReportStatusCalculator.new(:bit_field => status)
end
end
end
app/models/report.rb
class Report < ActiveRecord::Base
METRIC = %w[applied restarted failed failed_restarts skipped pending]
BIT_NUM = 6
MAX = (1 << BIT_NUM) -1 # maximum value per metric
LOG_LEVELS = %w[debug info notice warning err alert emerg crit]
include Foreman::STI
include Authorizable
include ConfigurationStatusScopedSearch
......
has_one :hostgroup, :through => :host
validates :host_id, :status, :presence => true
validates :reported_at, :presence => true, :uniqueness => {:scope => :host_id}
validates :reported_at, :presence => true, :uniqueness => {:scope => [:host_id, :type]}
scoped_search :in => :host, :on => :name, :complete_value => true, :rename => :host
scoped_search :in => :environment, :on => :name, :complete_value => true, :rename => :environment
......
scoped_search :in => :hostgroup, :on => :title, :complete_value => true, :rename => :hostgroup_title
scoped_search :on => :reported_at, :complete_value => true, :default_order => :desc, :rename => :reported, :only_explicit => true
scoped_search :on => :status, :offset => 0, :word_size => 4*BIT_NUM, :complete_value => {:true => true, :false => false}, :rename => :eventful
scoped_search_status 'applied', :on => :status, :rename => :applied
scoped_search_status 'restarted', :on => :status, :rename => :restarted
scoped_search_status 'failed', :on => :status, :rename => :failed
scoped_search_status 'failed_restarts', :on => :status, :rename => :failed_restarts
scoped_search_status 'skipped', :on => :status, :rename => :skipped
scoped_search_status 'pending', :on => :status, :rename => :pending
# search for a metric - e.g.:
# Report.with("failed") --> all reports which have a failed counter > 0
# Report.with("failed",20) --> all reports which have a failed counter > 20
scope :with, lambda { |*arg|
where("(#{report_status} >> #{HostStatus::ConfigurationStatus.bit_mask(arg[0].to_s)}) > #{arg[1] || 0}")
}
# returns reports for hosts in the User's filter set
scope :my_reports, lambda {
......
# with_changes
scope :interesting, -> { where("status <> 0") }
# 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 = case st
when Integer, Fixnum
st
when Hash
ReportStatusCalculator.new(:counters => st).calculate
else
raise Foreman::Exception(N_('Unsupported report status format'))
end
write_attribute(:status, s)
end
# extracts serialized metrics and keep them as a hash_with_indifferent_access
def metrics
return {} if read_attribute(:metrics).nil?
YAML.load(read_attribute(:metrics)).with_indifferent_access
end
......
"#{host.name} / #{reported_at}"
end
def config_retrieval
metrics[:time][:config_retrieval].round(2) rescue 0
end
def runtime
(metrics[:time][:total] || metrics[:time].values.sum).round(2) rescue 0
end
def self.import(report, proxy_id = nil)
ReportImporter.import(report, proxy_id)
end
# 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)
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).select(:status).each do |r|
metrics.each_key do |m|
metrics[m] += r.status_of(m)
end
end
list[host.name] = {:metrics => metrics, :id => host.id} if metrics.values.sum > 0
end
list
Foreman::Deprecation.deprecation_warning('1.13', "Report model has turned to be STI, please use child classes")
ConfigReportImporter.import(report, proxy_id)
end
# add sort by report time
......
cond = "reports.created_at < \'#{(Time.now.utc - timerange).to_formatted_s(:db)}\'"
cond += " and reports.status = #{status}" unless status.nil?
Log.joins(:report).where(:report_id => Report.where(cond)).delete_all
Log.joins(:report).where(:report_id => where(cond)).delete_all
Message.where("id not IN (#{Log.unscoped.select('DISTINCT message_id').to_sql})").delete_all
Source.where("id not IN (#{Log.unscoped.select('DISTINCT source_id').to_sql})").delete_all
count = Report.where(cond).delete_all
logger.info Time.now.to_s + ": Expired #{count} Reports"
count = where(cond).delete_all
logger.info Time.now.to_s + ": Expired #{count} #{to_s.underscore.humanize.pluralize}"
count
end
......
def no_report
false
end
def summaryStatus
return _("Failed") if error?
return _("Modified") if changes?
_("Success")
end
# puppet report status table column name
def self.report_status
"status"
end
delegate :error?, :changes?, :pending?, :status, :status_of, :to => :calculator
delegate(*METRIC, :to => :calculator)
def calculator
ReportStatusCalculator.new(:bit_field => read_attribute(self.class.report_status))
end
end
app/services/config_report_importer.rb
class ConfigReportImporter < ReportImporter
def self.authorized_smart_proxy_features
@authorized_smart_proxy_features ||= super + ['Puppet']
end
def report_name_class
ConfigReport
end
private
def create_report_and_logs
super
return report unless report.persisted?
# we update our host record, so we won't need to lookup the report information just to display the host list / info
host.update_attribute(:last_report, time) if host.last_report.nil? or host.last_report.utc < time
# Store all Puppet message logs
import_log_messages
# Check for errors
notify_on_report_error(:puppet_error_state)
end
def report_status
ConfigReportStatusCalculator.new(:counters => raw['status']).calculate
end
end
app/services/config_report_status_calculator.rb
class ConfigReportStatusCalculator
# converts a counters hash into a bit field
# expects a metrics_to_hash kind of counters
# see the report_processor for the implementation
def initialize(options = {})
@counters = options[:counters] || {}
@raw_status = options[:bit_field] || 0
end
# calculates the raw_status based on counters
def calculate
@raw_status = 0
counters.each do |type, value|
value = value.to_i # JSON does everything as strings
value = ConfigReport::MAX if value > ConfigReport::MAX # we store up to 2^BIT_NUM -1 values as we want to use only BIT_NUM bits.
@raw_status |= value << (ConfigReport::BIT_NUM * ConfigReport::METRIC.index(type))
end
raw_status
end
# returns metrics (counters) based on raw_status (aka bit field)
# to get status of specific metric, @see #status_of
def status
@status ||= begin
calculate if raw_status == 0
counters = Hash.new(0)
ConfigReport::METRIC.each do |m|
counters[m] = (raw_status || 0) >> (ConfigReport::BIT_NUM * ConfigReport::METRIC.index(m)) & ConfigReport::MAX
end
counters
end
end
def status_of(counter)
raise(Foreman::Exception.new(N_("invalid type %s"), counter)) unless ConfigReport::METRIC.include?(counter)
status[counter]
end
# returns true if total error metrics are > 0
def error?
status_of('failed') + status_of('failed_restarts') > 0
end
# returns true if total action metrics are > 0
def changes?
status_of('applied') + status_of('restarted') > 0
end
# returns true if there are any changes pending
def pending?
status_of('pending') > 0
end
# generate dynamically methods for all metrics
# e.g. applied failed ...
ConfigReport::METRIC.each do |method|
define_method method do
status_of(method)
end
end
private
attr_reader :raw_status, :counters
end
app/services/foreman/access_permissions.rb
}
end
permission_set.security_block :reports do |map|
map.permission :view_reports, {:reports => [:index, :show, :auto_complete_search],
:"api/v1/reports" => [:index, :show, :last],
:"api/v2/reports" => [:index, :show, :last]
}
map.permission :destroy_reports, {:reports => [:destroy],
:"api/v1/reports" => [:destroy],
:"api/v2/reports" => [:destroy]
}
map.permission :upload_reports, {:"api/v2/reports" => [:create] }
permission_set.security_block :config_reports do |map|
map.permission :view_config_reports, {:"api/v1/reports" => [:index, :show, :last],
:"api/v2/reports" => [:index, :show, :last],
:config_reports => [:index, :show, :auto_complete_search],
:"api/v2/config_reports" => [:index, :show, :last]
}
map.permission :destroy_config_reports, {:config_reports => [:destroy],
:"api/v1/reports" => [:destroy],
:"api/v2/reports" => [:destroy],
:"api/v2/config_reports" => [:destroy]
}
map.permission :upload_config_reports, {:"api/v2/reports" => [:create],
:"api/v2/config_reports" => [:create]}
end
permission_set.security_block :facts do |map|
app/services/menu/loader.rb
Manager.map :top_menu do |menu|
menu.sub_menu :monitor_menu, :caption => N_('Monitor') do
menu.item :dashboard, :caption => N_('Dashboard')
menu.item :reports, :caption => N_('Reports'),
:url_hash => {:controller => '/reports', :action => 'index', :search => 'eventful = true'}
menu.item :fact_values, :caption => N_('Facts')
menu.item :statistics, :caption => N_('Statistics')
menu.item :trends, :caption => N_('Trends')
menu.item :audits, :caption => N_('Audits')
menu.divider :caption => N_('Reports')
menu.item :reports, :caption => N_('Config management'),
:url_hash => {:controller => '/config_reports', :action => 'index', :search => 'eventful = true'}
menu.divider
end
menu.sub_menu :hosts_menu, :caption => N_('Hosts') do
app/services/report_importer.rb
delegate :logger, :to => :Rails
attr_reader :report
# When writing your own Report importer, provide feature(s) of authorized Smart Proxies
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 ]).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
def self.report_features
['Puppet', 'Chef Proxy']
# to be overriden in children
def report_name_class
Foreman::Deprecation.deprecation_warning('1.13', "Report model has turned to be STI, please use child classes")
Report
end
def initialize(raw, proxy_id = nil)
......
start_time = Time.now
logger.info "processing report for #{name}"
logger.debug { "Report: #{raw.inspect}" }
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")
return Report.new
create_report_and_logs
if report.persisted?
logger.info("Imported report for #{name} in #{(Time.now - start_time).round(2)} seconds")
host.refresh_statuses
end
# convert report status to bit field
st = ReportStatusCalculator.new(:counters => raw['status']).calculate
# we update our host record, so we won't need to lookup the report information just to display the host list / info
host.last_report = time if host.last_report.nil? or host.last_report.utc < time
# if proxy authentication is enabled and we have no puppet proxy set, use it.
host.puppet_proxy_id ||= proxy_id
# 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)
# and save our report
@report = Report.new(:host => host, :reported_at => time, :status => st, :metrics => raw['metrics'])
return report unless report.save
# Store all Puppet message logs
import_log_messages
# Check for errors
inspect_report
logger.info("Imported report for #{name} in #{(Time.now - start_time).round(2)} seconds")
host.refresh_statuses
end
private
......
end
end
def inspect_report
def report_status
raise NotImplementedError
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
......
owners = host.owner.present? ? host.owner.recipients_for(:puppet_error_state) : []
if owners.present?
logger.debug "sending alert to #{owners.map(&:login).join(',')}"
MailNotification[:puppet_error_state].deliver(report, :users => owners)
MailNotification[mail_error_state].deliver(report, :users => owners)
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'])
@report.save
@report
end
end
app/services/report_status_calculator.rb
class ReportStatusCalculator
# converts a counters hash into a bit field
# expects a metrics_to_hash kind of counters
# see the report_processor for the implementation
def initialize(options = {})
@counters = options[:counters] || {}
@raw_status = options[:bit_field] || 0
end
# calculates the raw_status based on counters
def calculate
@raw_status = 0
counters.each do |type, value|
value = value.to_i # JSON does everything as strings
value = Report::MAX if value > Report::MAX # we store up to 2^BIT_NUM -1 values as we want to use only BIT_NUM bits.
@raw_status |= value << (Report::BIT_NUM * Report::METRIC.index(type))
end
raw_status
end
# returns metrics (counters) based on raw_status (aka bit field)
# to get status of specific metric, @see #status_of
def status
@status ||= begin
calculate if raw_status == 0
counters = Hash.new(0)
Report::METRIC.each do |m|
counters[m] = (raw_status || 0) >> (Report::BIT_NUM * Report::METRIC.index(m)) & Report::MAX
end
counters
end
end
def status_of(counter)
raise(Foreman::Exception.new(N_("invalid type %s"), counter)) unless Report::METRIC.include?(counter)
status[counter]
end
# returns true if total error metrics are > 0
def error?
status_of('failed') + status_of('failed_restarts') > 0
end
# returns true if total action metrics are > 0
def changes?
status_of('applied') + status_of('restarted') > 0
end
# returns true if there are any changes pending
def pending?
status_of('pending') > 0
end
# generate dynamically methods for all metrics
# e.g. applied failed ...
Report::METRIC.each do |method|
define_method method do
status_of(method)
end
end
private
attr_reader :raw_status, :counters
end
app/views/api/v1/reports/show.json.rabl
end
node :summary do |report|
report.summaryStatus
end
report.summary_status
end
app/views/api/v2/config_reports/base.json.rabl
object @config_report
attributes :id, :host_id, :host_name, :reported_at, :status
app/views/api/v2/config_reports/create.json.rabl
object @config_report
extends "api/v2/config_reports/show"
app/views/api/v2/config_reports/index.json.rabl
collection @config_reports
extends "api/v2/config_reports/main"
app/views/api/v2/config_reports/main.json.rabl
object @config_report
extends "api/v2/config_reports/base"
attributes :metrics, :created_at, :updated_at
app/views/api/v2/config_reports/show.json.rabl
object @config_report
extends "api/v2/config_reports/main"
child :logs do
child :source do
attribute :value => :source
end
child :message do
attribute :value => :message
end
attribute :level
end
node :summary do |config_report|
config_report.summary_status
end
app/views/api/v2/config_reports/update.json.rabl
object @config_report
extends "api/v2/config_reports/show"
app/views/api/v2/reports/show.json.rabl
end
node :summary do |report|
report.summaryStatus
report.summary_status
end
app/views/config_reports/_list.html.erb
<table class="table table-bordered table-striped ellipsis">
<thead>
<tr>
<% unless params[:host_id] %>
<th><%= sort :host, :as => _("Host") %></th>
<% end %>
<th><%= sort :reported, :as => _("Last report") %></th>
<th class="col-md-1"><%= sort :applied, :as => _("Applied") %></th>
<th class="col-md-1"><%= sort :restarted, :as => _("Restarted") %></th>
<th class="col-md-1"><%= sort :failed, :as => _("Failed") %></th>
<th class="col-md-1"><%= sort :failed_restarts, :as => _("Restart<br>Failures").html_safe %></th>
<th class="col-md-1"><%= sort :skipped, :as => _("Skipped") %></th>
<th class="col-md-1"><%= sort :pending, :as => _("Pending") %></th>
<th class="col-md-1"></th>
</tr>
</thead>
<tbody>
<% for report in @config_reports %>
<tr>
<% if params[:host_id].nil? %>
<% if report.host.nil? %>
<td></td>
<% else %>
<td><%= link_to report.host, host_config_reports_path(report.host) %></td>
<% end %>
<% end %>
<td><%= reported_at_column(report) %></td>
<td><%= report_event_column(report.applied, "label-info") %></td>
<td><%= report_event_column(report.restarted, "label-info") %></td>
<td><%= report_event_column(report.failed, "label-danger") %></td>
<td><%= report_event_column(report.failed_restarts, "label-warning") %></td>
<td><%= report_event_column(report.skipped, "label-info") %></td>
<td><%= report_event_column(report.pending, "label-info") %></td>
<td align="right">
<%= display_delete_if_authorized hash_for_config_report_path(:id => report).merge(:auth_object => report, :authorizer => authorizer),
:confirm => _("Delete report for %s?") % report.host.try(:name) %>
</td>
</tr>
<% end %>
</tbody>
</table>
<%= will_paginate_with_info @config_reports %>
app/views/config_reports/_metrics.html.erb
<% @totaltime = metrics.delete('total') %>
<% metrics.delete_if{ |k,v| v < 0.001 } %>
<div class="row">
<div class="col-md-4">
<div class="stats-well">
<h4 class="ca" ><%= _('Report Metrics') %></h4>
<div style="margin-top:50px;padding-bottom: 40px;">
<%= flot_pie_chart("metrics" ,_("Report Metrics"), metrics, :class => "statistics-pie small")%>
</div>
</div>
</div>
<div class="col-md-4">
<div class="stats-well">
<h4 class="ca" ><%= _('Report Status') %></h4>
<%= flot_bar_chart("status" ,"", _("Number of Events"), status, :class => "statistics-bar")%>
</div>
</div>
<div class="col-md-4">
<table class='table table-bordered table-striped' style="height: 398px;">
<tbody>
<% metrics.sort.each do |title, value|%>
<tr>
<td class="break-me">
<%= title %>
</td>
<td>
<%= metric value %>
</td>
</tr>
<% end %>
</tbody>
<tfoot>
<tr>
<th><%= _("Total") %></th><th><%= metric (@totaltime || @config_report.runtime) %></th>
</tr>
</tfoot>
</table>
</div>
</div>
app/views/config_reports/_output.html.erb
<table id='report_log' class="table table-bordered table-striped">
<thead>
<tr>
<th><%= _("Level") %></th>
<th><%= _("Resource") %></th>
<th><%= _("message") %></th>
</tr>
</thead>
<tbody>
<% logs.each do |log| %>
<tr>
<td><span <%= report_tag log.level %>><%= h log.level %></span></td>
<td class="break-me"><%= h log.source %></td>
<td class="break-me"><%= h log.message %></td>
</tr>
<% end %>
<tr id='ntsh' <%= "style='display: none;'".html_safe if logs.size > 0%>><td colspan="3">
<%= _("Nothing to show") %>
</td></tr>
</tbody>
</table>
app/views/config_reports/index.html.erb
<% title _("Reports") %>
<% title_actions documentation_button('3.5.4PuppetReports') %>
<%= render :partial => 'list' %>
app/views/config_reports/show.html.erb
<%= javascript 'reports', 'ace/ace' %>
<% title "#{@config_report.host}"%>
<p class='ra'> <%= _("Reported at %s ") % @config_report.reported_at %> </p>
<% if @offset > 10 %>
<div class="alert alert-block alert-danger alert-dismissable">
<%= alert_close %>
<h3><%= _("Host times seems to be adrift!") %></h3>
<%= (_("Host reported time is <em>%s</em>") % @config_report.reported_at).html_safe %> <br/>
<%= (_("Foreman report creation time is <em>%s</em>") % @config_report.created_at).html_safe %> <br/>
<%= (_("Which is an offset of <b>%s</b>") % distance_of_time_in_words(@config_report.reported_at, @config_report.created_at, true)).html_safe %>
</div>
<% end %>
<% content_for(:search_bar) {logs_show} %>
<%= render 'output', :logs => @config_report.logs%>
<%= render 'metrics', :status => @config_report.status, :metrics => @config_report.metrics["time"] if @config_report.metrics["time"] %>
<%= title_actions link_to(_('Back'), :back),
display_delete_if_authorized(hash_for_config_report_path(:id => @config_report), :class=> "btn btn-danger"),
link_to(_("Host details"), @config_report.host),
link_to(_("Other reports for this host"), host_config_reports_path(@config_report.host))
%>
<div id="diff-modal" class="modal fade">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title"><%= _("Diff View") %></h4>
</div>
<div id="diff-modal-editor" class="modal-body">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal"><%= _("Close") %></button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
app/views/config_reports/welcome.html.erb
<% title _("Puppet Reports") %>
<div id="welcome">
<p>
<%= _("You don't seem to have any reports, if you wish to configure Puppet to forward it reports to Foreman, please follow") %>
<%= link_to _("setting up reporting"), "http://www.theforeman.org/manuals/#{SETTINGS[:version].short}/index.html#3.5.4PuppetReports", :rel => "external" %>
<%= _("and") %>
<%= link_to _("e-mail reporting"), "http://theforeman.org/projects/foreman/wiki/Mail_Notifications", :rel => "external" %>.
</p>
<p><%= _('This page will self destruct once a report comes in.') %></p>
</div>
app/views/dashboard/_reports_widget.html.erb
<tbody>
<% events.each do |report| %>
<tr>
<td><%= link_to trunc_with_tooltip(report.host, 14), host_reports_path(report.host) %></td>
<td><%= link_to trunc_with_tooltip(report.host, 14), host_config_reports_path(report.host) %></td>
<td><%= report_event_column(report.applied, "label-info") %></td>
<td><%= report_event_column(report.restarted, "label-info") %></td>
<td><%= report_event_column(report.failed, "label-danger") %></td>
app/views/host_mailer/_active_hosts.html.erb
<td style="<%= td_style %>">
<% end %>
<% if v > 0 %>
<%= link_to v, reports_path(:search=>"host = #{host} and #{m} > 0", :host => @url.host, :port => @url.port, :protocol => @url.scheme, :only_path => false) %>
<%= link_to v, config_reports_path(:search=>"host = #{host} and #{m} > 0", :host => @url.host, :port => @url.port, :protocol => @url.scheme, :only_path => false) %>
<% else %>
<%= v %>
<% end %>
app/views/reports/_list.html.erb
<table class="table table-bordered table-striped ellipsis">
<thead>
<tr>
<% unless params[:host_id] %>
<th><%= sort :host, :as => _("Host") %></th>
<% end %>
<th><%= sort :reported, :as => _("Last report") %></th>
<th class="col-md-1"><%= sort :applied, :as => _("Applied") %></th>
<th class="col-md-1"><%= sort :restarted, :as => _("Restarted") %></th>
<th class="col-md-1"><%= sort :failed, :as => _("Failed") %></th>
<th class="col-md-1"><%= sort :failed_restarts, :as => _("Restart<br>Failures").html_safe %></th>
<th class="col-md-1"><%= sort :skipped, :as => _("Skipped") %></th>
<th class="col-md-1"><%= sort :pending, :as => _("Pending") %></th>
<th class="col-md-1"></th>
</tr>
</thead>
<tbody>
<% for report in @reports %>
<tr>
<% if params[:host_id].nil? %>
<% if report.host.nil? %>
<td></td>
<% else %>
<td><%= link_to report.host, host_reports_path(report.host) %></td>
<% end %>
<% end %>
<td><%= reported_at_column(report) %></td>
<td><%= report_event_column(report.applied, "label-info") %></td>
<td><%= report_event_column(report.restarted, "label-info") %></td>
<td><%= report_event_column(report.failed, "label-danger") %></td>
<td><%= report_event_column(report.failed_restarts, "label-warning") %></td>
<td><%= report_event_column(report.skipped, "label-info") %></td>
<td><%= report_event_column(report.pending, "label-info") %></td>
<td align="right">
<%= display_delete_if_authorized hash_for_report_path(:id => report).merge(:auth_object => report, :authorizer => authorizer),
:confirm => _("Delete report for %s?") % report.host.try(:name) %>
</td>
</tr>
<% end %>
</tbody>
</table>
<%= will_paginate_with_info @reports %>
app/views/reports/_metrics.html.erb
<% @totaltime = metrics.delete('total') %>
<% metrics.delete_if{ |k,v| v < 0.001 } %>
<div class="row">
<div class="col-md-4">
<div class="stats-well">
<h4 class="ca" ><%= _('Report Metrics') %></h4>
<div style="margin-top:50px;padding-bottom: 40px;">
<%= flot_pie_chart("metrics" ,_("Report Metrics"), metrics, :class => "statistics-pie small")%>
</div>
</div>
</div>
<div class="col-md-4">
<div class="stats-well">
<h4 class="ca" ><%= _('Report Status') %></h4>
<%= flot_bar_chart("status" ,"", _("Number of Events"), status, :class => "statistics-bar")%>
</div>
</div>
<div class="col-md-4">
<table class='table table-bordered table-striped' style="height: 398px;">
<tbody>
<% metrics.sort.each do |title, value|%>
<tr>
<td class="break-me">
<%= title %>
</td>
<td>
<%= metric value %>
</td>
</tr>
<% end %>
</tbody>
<tfoot>
<tr>
<th><%= _("Total") %></th><th><%= metric (@totaltime || @report.runtime) %></th>
</tr>
</tfoot>
</table>
</div>
</div>
app/views/reports/_output.html.erb
<table id='report_log' class="table table-bordered table-striped">
<thead>
<tr>
<th><%= _("Level") %></th>
<th><%= _("Resource") %></th>
<th><%= _("message") %></th>
</tr>
</thead>
<tbody>
<% logs.each do |log| %>
<tr>
<td><span <%= report_tag log.level %>><%= h log.level %></span></td>
<td class="break-me"><%= h log.source %></td>
<td class="break-me"><%= h log.message %></td>
</tr>
<% end %>
<tr id='ntsh' <%= "style='display: none;'".html_safe if logs.size > 0%>><td colspan="3">
... This diff was truncated because it exceeds the maximum size that can be displayed.

Also available in: Unified diff