Project

General

Profile

« Previous | Next » 

Revision e54016da

Added by Marek Hulán over 8 years ago

Fixes #10782 - global host status

Introduce new global host status that is composed of host substatuses.
Each substatus defines a mapping to the global one which can result in
three values
  • OK
  • WARN
  • ERROR

Plugins can add their own substatuses. These are automatically
propagated also to API.

Thanks to Tomas Strachota who wrote the original code.

View differences:

app/assets/stylesheets/application.scss
.error-message{
padding-right: 10px;
}
i.glyphicon.host-status {
margin-right: 5px;
}
span.glyphicon.host-status {
top: 3px;
}
app/assets/stylesheets/status-colors.scss
.status-ok {
color: #5CB85C;
}
.status-error {
color: #D9534F
}
.status-warn, .status-question {
color: #DB843D;
}
app/controllers/api/v1/hosts_controller.rb
eos
def status
render :json => { :status => @host.host_status }.to_json if @host
Foreman::Deprecation.api_deprecation_warning('The /status route is deprecated, please use the new /status/configuration instead')
render :json => { :status => @host.get_status(HostStatus::ConfigurationStatus).to_label }.to_json if @host
end
private
app/controllers/api/v2/hosts_controller.rb
param_group :search_and_pagination, ::Api::V2::BaseController
def index
@hosts = resource_scope_for_index
@hosts = resource_scope_for_index.includes([ :host_statuses, :compute_resource, :hostgroup, :operatingsystem, :interfaces])
# 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)
end
api :GET, "/hosts/:id/", N_("Show a host")
......
process_response @host.destroy
end
api :GET, "/hosts/:id/status", N_("Get status of host")
api :GET, "/hosts/:id/status", N_("Get configuration status of host")
param :id, :identifier_dottable, :required => true
description <<-eos
Return value may either be one of the following:
......
eos
def status
render :json => { :status => @host.host_status }.to_json if @host
Foreman::Deprecation.api_deprecation_warning('The /status route is deprecated, please use the new /status/configuration instead')
render :json => { :status => @host.get_status(HostStatus::ConfigurationStatus).to_label }.to_json if @host
end
api :GET, "/hosts/:id/status/:type", N_("Get status of host")
param :id, :identifier_dottable, :required => true
param :type, [ HostStatus::Global ] + HostStatus.status_registry.to_a.map { |s| s.humanized_name }, :required => true, :desc => N_(<<-eos
status type, can be one of
* global
* configuration
* build
eos
)
description N_('Returns string representing a host status of a given type')
def get_status
case params[:type]
when 'global'
@status = @host.build_global_status
else
@status = @host.get_status(HostStatus.find_status_by_humanized_name(params[:type]))
end
end
api :GET, "/hosts/:id/vm_compute_attributes", N_("Get vm attributes of host")
......
:console
when 'disassociate'
:edit
when 'vm_compute_attributes'
when 'vm_compute_attributes', 'get_status'
:view
else
super
app/controllers/application_controller.rb
# If the user has a fact_filter then we need to include :fact_values
# We do not include most associations unless we are processing a html page
def included_associations(include = [])
include += [:hostgroup, :compute_resource, :operatingsystem, :environment, :model ]
include
include + [ :hostgroup, :compute_resource, :operatingsystem, :environment, :model, :host_statuses ]
end
def errors_hash(errors)
app/controllers/hosts_controller.rb
format.html do
@hosts = search.includes(included_associations).paginate(:page => params[:page])
# SQL optimizations queries
@last_reports = Report.where(:host_id => @hosts.map(&:id)).group(:host_id).maximum(:id)
@last_report_ids = Report.where(:host_id => @hosts.map(&:id)).group(:host_id).maximum(:id)
@last_reports = Report.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)
......
end
def review_before_build
@build = @host.build_status
@build = @host.build_status_checker
render :layout => false
end
app/helpers/hosts_helper.rb
def last_report_tooltip(record)
opts = { :rel => "twipsy" }
if @last_reports[record.id]
if @last_report_ids[record.id]
opts.merge!( "data-original-title" => _("View last report details"))
else
opts.merge!(:disabled => true, :class => "disabled", :onclick => 'return false')
......
end
# method that reformat the hostname column by adding the status icons
def name_column(record)
label = record.host_status
case label
when "Pending Installation"
style ="label-info"
# TRANSLATORS: host's status: first character of "build"
short = s_("Build|B")
when "Alerts disabled"
style = "label-default"
# TRANSLATORS: host's status: first character of "disabled"
short = s_("Disabled|D")
when "No reports"
style = "label-default"
# TRANSLATORS: host's status: first character of "no reports"
short = s_("No reports|N")
when "Out of sync"
style = "label-warning"
# TRANSLATORS: host's status: first character of "sync" (out of sync)
short = s_("Sync|S")
when "Error"
style = "label-danger"
# TRANSLATORS: host's status: first character of "error"
short = s_("Error|E")
when "Active"
style = "label-info"
# TRANSLATORS: host's status: first character of "active"
short = s_("Active|A")
when "Pending"
style = "label-warning"
# TRANSLATORS: host's status: first character of "pending"
short = s_("Pending|P")
else
style = "label-success"
# TRANSLATORS: host's status: first character of "OK"
short = s_("OK|O")
def name_column(host)
style = host_global_status_icon_class_for_host(host)
tooltip = host.host_statuses.select(&:relevant?).sort_by(&:type).map { |status| "#{_(status.name)}: #{_(status.to_label)}" }.join(', ')
content = content_tag(:span, "", {:rel => "twipsy", :class => style, :"data-original-title" => tooltip} )
content += link_to(trunc_with_tooltip(" #{host}"), host_path(host))
content
end
def host_global_status_icon_class_for_host(host)
options = {}
options[:last_reports] = @last_reports unless @last_reports.nil?
host_global_status_icon_class(host.build_global_status(options).status)
end
def host_global_status_icon_class(status)
icon_class = case status
when HostStatus::Global::OK
'glyphicon-ok-sign'
when HostStatus::Global::WARN
'glyphicon-info-sign'
when HostStatus::Global::ERROR
'glyphicon-exclamation-sign'
else
'glyphicon-question-sign'
end
"host-status glyphicon #{icon_class} #{host_global_status_class(status)}"
end
def host_global_status_class(status)
case status
when HostStatus::Global::OK
'status-ok'
when HostStatus::Global::WARN
'status-warn'
when HostStatus::Global::ERROR
'status-error'
else
'status-question'
end
content_tag(:span, short, {:rel => "twipsy", :class => "label label-light " + style, :"data-original-title" => _(label)} ) +
link_to(trunc_with_tooltip(" #{record}"), host_path(record))
end
def days_ago(time)
......
end
def overview_fields(host)
global_status = host.build_global_status
fields = [
[_("Status"), content_tag(:i, ''.html_safe, :class => host_global_status_icon_class(global_status.status)) +
content_tag(:span, _(global_status.to_label), :class => host_global_status_class(global_status.status))
]
]
fields += host_detailed_status_list(host)
fields += [
[_("Domain"), (link_to(host.domain, hosts_path(:search => "domain = #{host.domain}")) if host.domain)],
[_("Realm"), (link_to(host.realm, hosts_path(:search => "realm = #{host.realm}")) if host.realm)],
[_("IP Address"), host.ip],
......
fields
end
def host_detailed_status_list(host)
host.host_statuses.sort_by(&:type).map do |status|
next unless status.relevant?
[
_(status.name),
content_tag(:i, ' '.html_safe, :class => host_global_status_icon_class(status.to_global)) +
content_tag(:span, _(status.to_label), :class => host_global_status_class(status.to_global))
]
end
end
def possible_images(cr, arch = nil, os = nil)
return cr.images unless controller_name == "hosts"
return [] unless arch && os
app/models/concerns/configuration_status_scoped_search.rb
module ConfigurationStatusScopedSearch
extend ActiveSupport::Concern
module ClassMethods
def scoped_search_status(status, options)
options.merge!({ :offset => Report::METRIC.index(status.to_s), :word_size => Report::BIT_NUM })
scoped_search options
end
end
end
app/models/concerns/hostext/search.rb
included do
include ScopedSearchExtensions
include ConfigurationStatusScopedSearch
has_many :search_parameters, :class_name => 'Parameter', :foreign_key => :reference_id
belongs_to :search_users, :class_name => 'User', :foreign_key => :owner_id
......
scoped_search :on => :managed, :complete_value => {:true => true, :false => false}
scoped_search :on => :owner_type, :complete_value => true, :only_explicit => true
scoped_search :on => :owner_id, :complete_enabled => false, :only_explicit => true
scoped_search :on => :puppet_status, :offset => 0, :word_size => Report::BIT_NUM*4, :complete_value => {:true => true, :false => false}, :rename => :'status.interesting'
scoped_search :on => :puppet_status, :offset => Report::METRIC.index("applied"), :word_size => Report::BIT_NUM, :rename => :'status.applied'
scoped_search :on => :puppet_status, :offset => Report::METRIC.index("restarted"), :word_size => Report::BIT_NUM, :rename => :'status.restarted'
scoped_search :on => :puppet_status, :offset => Report::METRIC.index("failed"), :word_size => Report::BIT_NUM, :rename => :'status.failed'
scoped_search :on => :puppet_status, :offset => Report::METRIC.index("failed_restarts"), :word_size => Report::BIT_NUM, :rename => :'status.failed_restarts'
scoped_search :on => :puppet_status, :offset => Report::METRIC.index("skipped"), :word_size => Report::BIT_NUM, :rename => :'status.skipped'
scoped_search :on => :puppet_status, :offset => Report::METRIC.index("pending"), :word_size => Report::BIT_NUM, :rename => :'status.pending'
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_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'
scoped_search_status "failed_restarts", :in => :configuration_status_object, :on => :status, :rename => :'status.failed_restarts'
scoped_search_status "skipped", :in => :configuration_status_object, :on => :status, :rename => :'status.skipped'
scoped_search_status "pending", :in => :configuration_status_object, :on => :status, :rename => :'status.pending'
scoped_search :on => :global_status, :complete_value => { :ok => HostStatus::Global::OK, :warning => HostStatus::Global::WARN, :error => HostStatus::Global::ERROR }
scoped_search :in => :model, :on => :name, :complete_value => true, :rename => :model
scoped_search :in => :hostgroup, :on => :name, :complete_value => true, :rename => :hostgroup
app/models/concerns/report_common.rb
module ReportCommon
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]
extend ActiveSupport::Concern
included do
# 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, ->(*arg) { { :conditions => "(#{report_status} >> #{BIT_NUM*METRIC.index(arg[0])} & #{MAX}) > #{arg[1] || 0}"} }
end
# generate dynamically methods for all metrics
# e.g. Report.last.applied
METRIC.each do |method|
define_method method do
status method
end
end
# returns true if total error metrics are > 0
def error?
%w[failed failed_restarts].sum {|f| status f} > 0
end
# returns true if total action metrics are > 0
def changes?
%w[applied restarted].sum {|f| status f} > 0
end
# returns true if there are any changes pending
def pending?
pending > 0
end
#returns metrics
#when no metric type is specific returns hash with all values
#passing a METRIC member will return its value
def status(type = nil)
@calc ||= ReportStatusCalculator.new(:bit_field => read_attribute(self.class.report_status))
@calc.status(type)
end
end
app/models/host/hostmix.rb
belongs_to :host, {:class_name => "Host::Managed", :foreign_key => :host_id}.merge(options)
end
end
end
end
app/models/host/managed.rb
class Host::Managed < Host::Base
include ReportCommon
include Hostext::Search
PROVISION_METHODS = %w[build image]
......
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 :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
......
belongs_to :owner, :polymorphic => true
belongs_to :compute_resource
belongs_to :image
has_many :host_statuses, :class_name => 'HostStatus::Status', :foreign_key => 'host_id', :inverse_of => :host,
:dependent => :destroy
has_one :configuration_status_object, :class_name => 'HostStatus::ConfigurationStatus', :foreign_key => 'host_id'
has_one :token, :foreign_key => :host_id, :dependent => :destroy
before_destroy :remove_reports
......
define_model_callbacks :build, :only => :after
define_model_callbacks :provision, :only => :before
before_validation :refresh_build_status, :if => :build_changed?
# Custom hooks will be executed after_commit
after_commit :build_hooks
before_save :clear_data_on_build
......
scope :with_os, -> { where('hosts.operatingsystem_id IS NOT NULL') }
scope :with_status, lambda { |status_type|
includes(:host_statuses).where("host_status.type = '#{status_type}'")
}
scope :with_config_status, lambda {
with_status('HostStatus::ConfigurationStatus')
}
# search for a metric - e.g.:
# Host::Managed.with("failed") --> all reports which have a failed counter > 0
# Host::Managed.with("failed",20) --> all reports which have a failed counter > 20
scope :with, lambda { |*arg|
with_config_status.where("(host_status.status >> #{HostStatus::ConfigurationStatus.bit_mask(arg[0].to_s)}) > #{arg[1] || 0}")
}
scope :with_error, lambda {
where("(puppet_status > 0) and
( ((puppet_status >> #{BIT_NUM*METRIC.index("failed")} & #{MAX}) != 0) or
((puppet_status >> #{BIT_NUM*METRIC.index("failed_restarts")} & #{MAX}) != 0) )")
with_config_status.where("(host_status.status > 0) and (
#{HostStatus::ConfigurationStatus.is('failed')} or
#{HostStatus::ConfigurationStatus.is('failed_restarts')}
)")
}
scope :without_error, lambda {
where("((puppet_status >> #{BIT_NUM*METRIC.index("failed")} & #{MAX}) = 0) and
((puppet_status >> #{BIT_NUM*METRIC.index("failed_restarts")} & #{MAX}) = 0)")
with_config_status.where("
#{HostStatus::ConfigurationStatus.is_not('failed')} and
#{HostStatus::ConfigurationStatus.is_not('failed_restarts')}
")
}
scope :with_changes, lambda {
where("(puppet_status > 0) and
( ((puppet_status >> #{BIT_NUM*METRIC.index("applied")} & #{MAX}) != 0) or
((puppet_status >> #{BIT_NUM*METRIC.index("restarted")} & #{MAX}) != 0) )")
with_config_status.where("(host_status.status > 0) and (
#{HostStatus::ConfigurationStatus.is('applied')} or
#{HostStatus::ConfigurationStatus.is('restarted')}
)")
}
scope :without_changes, lambda {
where("((puppet_status >> #{BIT_NUM*METRIC.index("applied")} & #{MAX}) = 0) and
((puppet_status >> #{BIT_NUM*METRIC.index("restarted")} & #{MAX}) = 0)")
with_config_status.where("
#{HostStatus::ConfigurationStatus.is_not('applied')} and
#{HostStatus::ConfigurationStatus.is_not('restarted')}
")
}
scope :with_pending_changes, -> { where("(puppet_status > 0) and ((puppet_status >> #{BIT_NUM*METRIC.index("pending")} & #{MAX}) != 0)") }
scope :without_pending_changes, -> { where("((puppet_status >> #{BIT_NUM*METRIC.index("pending")} & #{MAX}) = 0)") }
scope :with_pending_changes, lambda {
with_config_status.where("(host_status.status > 0) AND (#{HostStatus::ConfigurationStatus.is('pending')})")
}
scope :without_pending_changes, lambda {
with_config_status.where("#{HostStatus::ConfigurationStatus.is_not('pending')}")
}
scope :successful, -> { without_changes.without_error.without_pending_changes}
......
if self.compute_resource
host.compute_attributes = host.compute_resource.vm_compute_attributes_for(self.uuid)
end
host.puppet_status = 0
host.refresh_global_status
host
end
......
compute_resource ? compute_resource.vm_compute_attributes_for(uuid) : nil
end
def host_status
if build
N_("Pending Installation")
elsif respond_to?(:enabled) && !enabled
N_("Alerts disabled")
elsif respond_to?(:last_report) && last_report.nil?
N_("No reports")
elsif no_report
N_("Out of sync")
elsif error?
N_("Error")
elsif changes?
N_("Active")
elsif pending?
N_("Pending")
else
N_("No changes")
end
end
def smart_proxies
SmartProxy.where(:id => smart_proxy_ids)
end
......
unattended_render(template)
end
def build_status
def build_status_checker
build_status = HostBuildStatus.new(self)
build_status.check_all_statuses
build_status
......
@old = super { |clone| clone.interfaces = self.interfaces.map {|i| setup_object_clone(i) } }
end
def refresh_global_status
self.global_status = build_global_status.status
end
def refresh_statuses
HostStatus.status_registry.each do |status_class|
status = get_status(status_class)
status.refresh! if status.relevant?
end
host_statuses.reload
refresh_global_status
end
def get_status(type)
status = self.new_record? ? host_statuses.detect { |s| s.type == type.to_s } : host_statuses.find_by_type(type.to_s)
if status.nil?
host_statuses.new(:host => self, :type => type.to_s)
else
status
end
end
def build_global_status(options = {})
HostStatus::Global.build(host_statuses, options)
end
def global_status_label(options = {})
HostStatus::Global.build(host_statuses, options).to_label
end
def configuration_status(options = {})
@configuration_status ||= get_status(HostStatus::ConfigurationStatus).to_status(options)
end
def configuration_status_label(options = {})
@configuration_status_label ||= get_status(HostStatus::ConfigurationStatus).to_label(options)
end
def puppet_status
Foreman::Deprecation.deprecation_warning('1.12', 'Host#puppet_status has been deprecated, you should use configuration_status')
configuration_status
end
def build_status(options = {})
@build_status ||= get_status(HostStatus::BuildStatus).to_status(options)
end
def build_status_label(options = {})
@build_status_label ||= get_status(HostStatus::BuildStatus).to_label(options)
end
private
# validate uniqueness can't prevent saving two interfaces that has same DNS name
......
status
end
# alias to ensure same method that resolves the last report between the hosts and reports tables.
def reported_at
last_report
end
# puppet report status table column name
def self.report_status
"puppet_status"
end
# converts a name into ip address using DNS.
# if we are managing DNS, we can query the correct DNS server
# otherwise, use normal systems dns settings to resolv
......
self.config_groups = []
end
end
def refresh_build_status
self.get_status(HostStatus::BuildStatus).refresh
end
end
app/models/host_status.rb
module HostStatus
def self.status_registry
@status_registry ||= Set.new
end
def self.find_status_by_humanized_name(name)
status_registry.find { |s| s.humanized_name == name }
end
end
require_dependency 'host_status/status'
app/models/host_status/build_status.rb
module HostStatus
class BuildStatus < Status
PENDING = 1
BUILT = 0
def self.status_name
N_("Build")
end
def to_label(options = {})
case to_status
when PENDING
N_("Pending installation")
when BUILT
N_("Installed")
else
N_("Unknown build status")
end
end
def to_status(options = {})
if waiting_for_build?
PENDING
else
BUILT
end
end
def relevant?
SETTINGS[:unattended] && host.managed?
end
def waiting_for_build?
host && host.build
end
end
end
HostStatus.status_registry.add(HostStatus::BuildStatus)
app/models/host_status/configuration_status.rb
module HostStatus
class ConfigurationStatus < Status
delegate :error?, :changes?, :pending?, :to => :calculator
delegate(*Report::METRIC, :to => :calculator)
def last_report
self.last_report = host.last_report_object unless @last_report_set
@last_report
end
def last_report=(report)
@last_report_set = true
@last_report = report
end
def out_of_sync?
if (host && !host.enabled?) || no_reports?
false
else
!reported_at.nil? && reported_at < (Time.now - (Setting[:puppet_interval] + Setting[:outofsync_interval]).minutes)
end
end
def no_reports?
host && last_report.nil?
end
def to_global(options = {})
handle_options(options)
if error?
# error
return HostStatus::Global::ERROR
elsif out_of_sync?
# out of sync
return HostStatus::Global::WARN
else
# active, pending, no changes, no reports
return HostStatus::Global::OK
end
end
def self.status_name
N_("Configuration")
end
def to_label(options = {})
handle_options(options)
if host && !host.enabled
N_("Alerts disabled")
elsif no_reports?
N_("No reports")
elsif error?
N_("Error")
elsif out_of_sync?
N_("Out of sync")
elsif changes?
N_("Active")
elsif pending?
N_("Pending")
else
N_("No changes")
end
end
def to_status(options = {})
handle_options(options)
if host && last_report.present?
last_report.read_attribute(:status)
else
0
end
end
def self.is(config_status)
"((host_status.status >> #{bit_mask(config_status)}) != 0)"
end
def self.is_not(config_status)
"((host_status.status >> #{bit_mask(config_status)}) = 0)"
end
def self.bit_mask(config_status)
"#{Report::BIT_NUM * Report::METRIC.index(config_status)} & #{Report::MAX}"
end
private
def handle_options(options)
if options.has_key?(:last_reports) && !options[:last_reports].nil?
cached_report = options[:last_reports].find { |r| r.host_id == self.host_id }
self.last_report = cached_report
end
end
def update_timestamp
self.reported_at = last_report.try(:reported_at) || Time.now
end
def calculator
ReportStatusCalculator.new(:bit_field => status)
end
end
end
HostStatus.status_registry.add(HostStatus::ConfigurationStatus)
app/models/host_status/global.rb
module HostStatus
class Global
OK = 0
WARN = 1
ERROR = 2
attr_accessor :status
def self.build(statuses, options = {})
max_status = statuses.select { |s| s.relevant? }.map { |s| s.to_global(options) }.max
new(max_status || OK)
end
def self.status_name
N_('Global')
end
def name
self.class.status_name
end
def initialize(status)
self.status = status
end
def to_label
case status
when OK
N_('OK')
when WARN
N_('Warning')
when ERROR
N_('Error')
else
raise 'Unknown global status'
end
end
end
end
app/models/host_status/status.rb
module HostStatus
class Status < ActiveRecord::Base
include Foreman::STI
self.table_name = 'host_status'
belongs_to_host :inverse_of => :host_statuses
attr_accessible :host, :type
def to_global(options = {})
HostStatus::Global::OK
end
def to_label(options = {})
raise NotImplementedError, "Method 'to_label' method needs to be implemented"
end
def to_status(options = {})
raise NotImplementedError, "Method 'to_status' method needs to be implemented"
end
def self.status_name
raise NotImplementedError, "Method 'status_name' method needs to be implemented"
end
def name
self.class.status_name
end
def self.humanized_name
status_name.underscore
end
def refresh!
refresh
save!
end
def refresh
update_timestamp
update_status
end
def relevant?
true
end
private
def update_timestamp
self.reported_at = Time.now
end
def update_status
self.status = to_status
end
end
end
require_dependency 'host_status/configuration_status'
require_dependency 'host_status/build_status'
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 Authorizable
include ReportCommon
include ConfigurationStatusScopedSearch
validates_lengths_from_database
belongs_to_host
......
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 :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
scoped_search :on => :status, :offset => METRIC.index("pending"), :word_size => BIT_NUM, :rename => :pending
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 {
......
else
raise Foreman::Exception(N_('Unsupported report status format'))
end
@calc = nil
write_attribute(:status,s)
write_attribute(:status, s)
end
# extracts serialized metrics and keep them as a hash_with_indifferent_access
......
METRIC.each {|m| metrics[m] = 0 }
host.reports.recent(time).select(:status).each do |r|
metrics.each_key do |m|
metrics[m] += r.status(m)
metrics[m] += r.status_of(m)
end
end
list[host.name] = {:metrics => metrics, :id => host.id} if metrics.values.sum > 0
......
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/foreman/access_permissions.rb
:dashboard => [:OutOfSync, :errors, :active],
:unattended => [:template, :provision],
:"api/v1/hosts" => [:index, :show, :status],
:"api/v2/hosts" => [:index, :show, :status, :vm_compute_attributes],
:"api/v2/hosts" => [:index, :show, :status, :get_status, :vm_compute_attributes],
:"api/v2/interfaces" => [:index, :show],
:locations => [:mismatches],
:organizations => [:mismatches]
app/services/foreman/plugin.rb
end
@apipie_ignored_controllers
end
# register custom host status class, it should inherit from HostStatus::Status
def register_custom_status(klass)
HostStatus.status_registry.add(klass)
end
end
end
app/services/report_importer.rb
end
# convert report status to bit field
st = ReportStatusCalculator.new(:counters => raw['status']).calculate
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
# we save the report bit status value in our host too.
host.puppet_status = st
# if proxy authentication is enabled and we have no puppet proxy set, use it.
host.puppet_proxy_id ||= proxy_id
......
# Check for errors
inspect_report
logger.info("Imported report for #{name} in #{(Time.now - start_time).round(2)} seconds")
host.refresh_statuses
end
private
app/services/report_status_calculator.rb
@raw_status = options[:bit_field] || 0
end
# calculates the raw_status based on counters
def calculate
@raw_status = 0
counters.each do |type, value|
......
raw_status
end
#returns metrics
#when no metric type is specific returns hash with all values
#passing a METRIC member will return its value
def status(type = nil)
calculate if raw_status == 0
raise(Foreman::Exception(N_("invalid type %s") % type)) if type && !Report::METRIC.include?(type)
counters = Hash.new(0)
(type.is_a?(String) ? [type] : Report::METRIC).each do |m|
counters[m] = (raw_status || 0) >> (Report::BIT_NUM * Report::METRIC.index(m)) & Report::MAX
# 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
type.nil? ? counters : counters[type]
end
private
attr_reader :raw_status, :counters
end
end
app/views/api/v2/hosts/get_status.json.rabl
object @status
attributes :to_label => :status_label
attributes :status
app/views/api/v2/hosts/main.json.rabl
extends "api/v2/hosts/base"
# we need to cache results with @last_reports, rabl can't pass custom parameters to attriute methods
@object.global_status_label(:last_reports => @last_reports)
@object.configuration_status(:last_reports => @last_reports)
@object.configuration_status_label(:last_reports => @last_reports)
attributes :ip, :environment_id, :environment_name, :last_report, :mac, :realm_id, :realm_name,
:sp_mac, :sp_ip, :sp_name, :domain_id, :domain_name, :architecture_id, :architecture_name, :operatingsystem_id, :operatingsystem_name,
:subnet_id, :subnet_name, :sp_subnet_id, :ptable_id, :ptable_name, :medium_id, :medium_name, :build,
......
:enabled, :puppet_ca_proxy_id, :managed, :use_image, :image_file, :uuid, :compute_resource_id, :compute_resource_name,
:compute_profile_id, :compute_profile_name, :capabilities, :provision_method,
:puppet_proxy_id, :certname, :image_id, :image_name, :created_at, :updated_at,
:last_compile, :puppet_status
:last_compile, :global_status, :global_status_label
attributes :organization_id, :organization_name if SETTINGS[:organizations_enabled]
attributes :location_id, :location_name if SETTINGS[:locations_enabled]
# to avoid deprecation warning on puppet_status method
attributes :configuration_status => :puppet_status
HostStatus.status_registry.each do |status_class|
attributes "#{status_class.humanized_name}_status", "#{status_class.humanized_name}_status_label", :if => @object.get_status(status_class).relevant?
end
config/application.rb
config.autoload_paths += %W(#{config.root}/app/models/auth_sources)
config.autoload_paths += %W(#{config.root}/app/models/compute_resources)
config.autoload_paths += %W(#{config.root}/app/models/lookup_keys)
config.autoload_paths += %W(#{config.root}/app/models/host_status)
config.autoload_paths += %W(#{config.root}/app/models/operatingsystems)
config.autoload_paths += %W(#{config.root}/app/models/parameters)
config.autoload_paths += %W(#{config.root}/app/models/trends)
config/routes/api/v2.rb
end
resources :hosts, :except => [:new, :edit] do
get :status, :on => :member
get 'status/:type', :on => :member, :action => :get_status
get :vm_compute_attributes, :on => :member
put :puppetrun, :on => :member
put :disassociate, :on => :member
db/migrate/20150612135546_create_host_status.rb
class CreateHostStatus < ActiveRecord::Migration
def up
create_table :host_status do |t|
t.string :type
t.integer :status, :default => 0, :null => false
t.references :host, :null => false
t.datetime :reported_at, :null => false
end
add_index :host_status, :host_id
add_foreign_key "host_status", "hosts", :name => "host_status_hosts_host_id_fk", :column => 'host_id'
add_column :hosts, :global_status, :integer, :default => 0, :null => false
Host.all.each do |host|
host.refresh_statuses
end
remove_column :hosts, :puppet_status
end
def down
add_column :hosts, :puppet_status, :integer, :null => false, :default => 0
remove_column :hosts, :global_status
remove_foreign_key "host_status", :name => "host_status_hosts_host_id_fk"
remove_index :host_status, :host_id
Host.all.each do |host|
config_status = host.host_statuses.find_by_type("HostStatus::ConfigurationStatus")
unless config_status.nil?
host.puppet_status = config_status.status
host.save
end
end
drop_table :host_status
end
end
test/functional/api/v2/hosts_controller_test.rb
assert_response :success
end
test "should show specific status hosts" do
get :get_status, { :id => @host.to_param, :type => 'global' }
assert_response :success
end
test "should be able to create hosts even when restricted" do
disable_orchestration
assert_difference('Host.count') do
test/unit/host_build_status_test.rb
:domain => domains(:mydomain), :operatingsystem => operatingsystems(:redhat), :subnet => subnets(:one), :puppet_proxy => smart_proxies(:puppetmaster),
:architecture => architectures(:x86_64), :environment => environments(:production), :managed => true,
:owner_type => "User", :root_pass => "xybxa6JUkz63w")
@build = @host.build_status
@build = @host.build_status_checker
# bypass host.valid?
HostBuildStatus.any_instance.stubs(:host_status).returns(true)
end
......
host = @host
kind = FactoryGirl.create(:template_kind)
FactoryGirl.create(:provisioning_template, :template => "provision script <%= @foreman.server.status %>",:name => "My Failed Template", :template_kind => kind, :operatingsystem_ids => [host.operatingsystem_id], :environment_ids => [host.environment_id], :hostgroup_ids => [host.hostgroup_id] )
@build = host.build_status
@build = host.build_status_checker
refute_empty @build.errors[:templates]
end
test/unit/host_status/build_status_test.rb
require 'test_helper'
class BuildStatusTest < ActiveSupport::TestCase
def setup
@host = FactoryGirl.build(:host)
@status = HostStatus::BuildStatus.new
@status.host = @host
end
test '#to_label changes based on waiting_for_build?' do
@status.stub(:waiting_for_build?, true) do
assert_equal 'Pending installation', @status.to_label
end
@status.stub(:waiting_for_build?, false) do
assert_equal 'Installed', @status.to_label
end
end
test '#relevant? is only for managed hosts in unattended mode' do
@host.managed = true
assert @status.relevant?
original, SETTINGS[:unattended] = SETTINGS[:unattended], false
refute @status.relevant?
SETTINGS[:unattended] = original
@host.managed = false
refute @status.relevant?
end
test '#waiting_for_build? verifies build flag and host relation' do
refute @status.waiting_for_build?
@status.host.build = true
assert @status.waiting_for_build?
@status.host = nil
refute @status.waiting_for_build?
end
end
test/unit/host_status/configuration_status_test.rb
require 'test_helper'
class ConfigurationStatusTest < ActiveSupport::TestCase
def setup
@host = FactoryGirl.create(:host)
@report = @host.reports.build
@report.status = {"applied" => 92, "restarted" => 300, "failed" => 4, "failed_restarts" => 12, "skipped" => 3, "pending" => 0}
@report.reported_at = '2015-01-01 00:00:00'
@report.save
@status = HostStatus::ConfigurationStatus.new(:host => @host)
@status.refresh!
end
test '#last_report defaults to host\'s last if nothing was set yet' do
assert_equal @report, @status.last_report
end
test '#last_report returns custom value that was set using writer method' do
@status.last_report = :something
assert_equal :something, @status.last_report
end
test '#last_report returns custom value that was set using writer method even for nil' do
@status.last_report = nil
assert_nil @status.last_report
end
test '#out_of_sync? is false if host reporting is disabled' do
assert @status.out_of_sync?
@host.enabled = false
refute @status.out_of_sync?
end
test '#out_of_sync? is true if reported_at is set and is too long ago' do
assert @status.reported_at.present?
window = (Setting[:puppet_interval] + Setting[:outofsync_interval]).minutes
assert @status.reported_at < Time.now - window
assert @status.out_of_sync?
end
test '#out_of_sync? is false when reported_at is unknown' do
@status.reported_at = nil
refute @status.out_of_sync?
end
test '#out_of_sync? is false when window is big enough' do
original, Setting[:outofsync_interval] = Setting[:outofsync_interval], (Time.now - @report.reported_at).to_i / 60 + 1
refute @status.out_of_sync?
Setting[:outofsync_interval] = original
end
test '#refresh! refreshes the date and persists the record' do
@status.expects(:refresh)
@status.refresh!
assert @status.persisted?
end
test '#refresh updates date to reported_at of last report' do
@status.reported_at = nil
@status.refresh
assert_equal @report.reported_at, @status.reported_at
end
test '.is_not' do
assert_equal '((host_status.status >> 6 & 63) = 0)', HostStatus::ConfigurationStatus.is_not('restarted')
end
test '.is' do
assert_equal '((host_status.status >> 6 & 63) != 0)', HostStatus::ConfigurationStatus.is('restarted')
end
test '.bit_mask' do
assert_equal '0 & 63', HostStatus::ConfigurationStatus.bit_mask('applied')
assert_equal '6 & 63', HostStatus::ConfigurationStatus.bit_mask('restarted')
assert_equal '12 & 63', HostStatus::ConfigurationStatus.bit_mask('failed')
end
end
test/unit/host_status/global_test.rb
require 'test_helper'
class GlobalTest < ActiveSupport::TestCase
class StatusMock < Struct.new(:global, :relevant)
alias_method :relevant?, :relevant
def to_global(options = {})
global
end
end
def setup
@status1 = StatusMock.new(HostStatus::Global::WARN, true)
@status2 = StatusMock.new(HostStatus::Global::ERROR, true)
@status3 = StatusMock.new(HostStatus::Global::OK, true)
end
test '.build(statuses) builds new global status with highest status code' do
global = HostStatus::Global.build([@status1, @status2, @status3])
assert_equal HostStatus::Global::ERROR, global.status
end
test '.build(statuses, :last_reports => [reports]) uses reports cache for configuration statuses' do
status = HostStatus::ConfigurationStatus.new
report = Report.new(:host_id => 1)
status.expects(:relevant?).returns(true)
status.expects(:to_global).returns(:result)
global = HostStatus::Global.build([ status ], :last_reports => [ report ])
assert_equal :result, global.status
end
test '.to_label returns string representation of status code' do
global = HostStatus::Global.new(HostStatus::Global::OK)
assert_kind_of String, global.to_label
end
end
test/unit/host_status_test.rb
require 'test_helper'
class HostStatusTest < ActiveSupport::TestCase
class DummyStatus < HostStatus::Status
def self.status_name
N_("DummyStatus")
end
end
test '.status_registry allows adding new status and recalling it later' do
status = OpenStruct.new
HostStatus.status_registry.add(status)
assert_includes HostStatus.status_registry, status
HostStatus.status_registry.delete(status)
refute_includes HostStatus.status_registry, status
end
test '.find_status_by_humanized_name' do
assert_equal HostStatus::ConfigurationStatus, HostStatus.find_status_by_humanized_name('configuration')
HostStatus.status_registry.add(DummyStatus)
assert_equal DummyStatus, HostStatus.find_status_by_humanized_name('dummy_status')
HostStatus.status_registry.delete(DummyStatus)
refute_includes HostStatus.status_registry, DummyStatus
end
end
test/unit/host_test.rb
assert_nil host
end
test 'host #refresh_global_status defaults to OK' do
host = FactoryGirl.build(:host)
... This diff was truncated because it exceeds the maximum size that can be displayed.

Also available in: Unified diff