Project

General

Profile

« Previous | Next » 

Revision 3484613f

Added by Joseph Magen almost 11 years ago

  • ID 3484613f7fb711c65724261720048218427fe670

fixes #2411 - move files in /models to /concerns, /services, /mailers, /observers

View differences:

app/controllers/api/api_responder.rb
module Api
class ApiResponder < ActionController::Responder
# overview api_behavior
def api_behavior(error)
raise error unless resourceful?
if !get? && !post?
#return resource instead of default "head :no_content" for PUT, PATCH, and DELETE
display resource
else
super
end
end
end
end
app/mailers/host_mailer.rb
class HostMailer < ActionMailer::Base
helper :reports
default :content_type => "text/html", :from => Setting[:email_reply_address] || "noreply@foreman.exmaple.org"
# sends out a summary email of hosts and their metrics (e.g. how many changes failures etc).
def summary(options = {})
# currently we send to all registered users or to the administrator (if LDAP is disabled).
# TODO add support to host / group based emails.
# options our host list if required
filter = []
if (@url = Setting[:foreman_url]).empty?
raise ::Foreman::Exception.new N_("':foreman_url:' entry in Foreman configuration file, see http://theforeman.org/projects/foreman/wiki/Mail_Notifications")
end
if options[:env]
hosts = envhosts = options[:env].hosts
raise (_("unable to find any hosts for puppet environment=%s") % env) if envhosts.size == 0
filter << "Environment=#{options[:env].name}"
end
name,value = options[:factname],options[:factvalue]
if name and value
facthosts = Host.search_for("facts.#{name}=#{value}")
raise (_("unable to find any hosts with the fact name=%{name} and value=%{value}") % { :name => name, :value => value }) if facthosts.empty?
filter << "Fact #{name}=#{value}"
# if environment and facts are defined together, we use a merge of both
hosts = envhosts.empty? ? facthosts : envhosts & facthosts
end
if hosts.empty?
# print out an error if we couldn't find any hosts that match our request
raise ::Foreman::Exception.new(N_("unable to find any hosts that match your request")) if options[:env] or options[:factname]
# we didnt define a filter, use all hosts instead
hosts = Host::Managed
end
email = options[:email] || Setting[:administrator]
raise ::Foreman::Exception.new(N_("unable to find recipients")) if email.empty?
time = options[:time] || 1.day.ago
host_data = Report.summarise(time, hosts.all).sort
total_metrics = {"failed"=>0, "restarted"=>0, "skipped"=>0, "applied"=>0, "failed_restarts"=>0}
host_data.flatten.delete_if { |x| true unless x.is_a?(Hash) }.each do |data_hash|
total_metrics["failed"] += data_hash[:metrics]["failed"]
total_metrics["restarted"] += data_hash[:metrics]["restarted"]
total_metrics["skipped"] += data_hash[:metrics]["skipped"]
total_metrics["applied"] += data_hash[:metrics]["applied"]
total_metrics["failed_restarts"] += data_hash[:metrics]["failed_restarts"]
end
total = 0 ; total_metrics.values.each { |v| total += v }
subject = _("Summary Puppet report from Foreman - F:%{failed} R:%{restarted} S:%{skipped} A:%{applied} FR:%{failed_restarts} T:%{total}") % {
:failed => total_metrics["failed"],
:restarted => total_metrics["restarted"],
:skipped => total_metrics["skipped"],
:applied => total_metrics["applied"],
:failed_restarts => total_metrics["failed_restarts"],
:total => total_metrics["total"]
}
@hosts = host_data
@timerange = time
@out_of_sync = hosts.out_of_sync
@disabled = hosts.alerts_disabled
@filter = filter
mail(:to => email, :subject => subject)
end
def error_state(report)
host = report.host
email = host.owner.recipients if SETTINGS[:login] and not host.owner.nil?
email = Setting[:administrator] if email.empty?
raise ::Foreman::Exception.new(N_("unable to find recipients")) if email.empty?
@report = report
@host = host
mail(:to => email, :subject => (_("Puppet error on %s") % host.to_label))
end
end
app/models/authorization.rb
module Authorization
def self.included(base)
base.class_eval do
before_save :enforce_edit_permissions
before_destroy :enforce_destroy_permissions
before_create :enforce_create_permissions
end
end
# We must enforce the security model
def enforce_edit_permissions
enforce_permissions("edit") if enforce?
end
def enforce_destroy_permissions
enforce_permissions("destroy") if enforce?
end
def enforce_create_permissions
enforce_permissions("create") if enforce?
end
def enforce_permissions operation
# We get called again with the operation being set to create
return true if operation == "edit" and new_record?
klass = self.class.name.downcase
klasses = self.class.name.tableize
klasses.gsub!(/auth_source.*/, "authenticators")
klasses.gsub!(/common_parameters.*/, "global_variables")
klasses.gsub!(/lookup_key.*/, "external_variables")
klasses.gsub!(/lookup_value.*/, "external_variables")
return true if User.current and User.current.allowed_to?("#{operation}_#{klasses}".to_sym)
errors.add :base, _("You do not have permission to %{operation} this %{klass}") % { :operation => operation, :klass => klass }
@permission_failed = operation
false
end
# @return false or name of failed operation
def permission_failed?
return false unless @permission_failed
@permission_failed
end
private
def enforce?
return false if (User.current and User.current.admin?)
return true if defined?(Rake) and Rails.env == "test"
return false if defined?(Rake)
true
end
end
app/models/classification/base.rb
module Classification
class Base
delegate :hostgroup, :environment_id,
:to => :host
def initialize args = { }
@host = args[:host]
end
#override to return the relevant enc data and format
def enc
raise NotImplementedError
end
def inherited_values
values_hash :skip_fqdn => true
end
protected
attr_reader :host
#override this method to return the relevant parameters for a given set of classes
def class_parameters
raise NotImplementedError
end
def puppetclass_ids
return @puppetclass_ids if @puppetclass_ids
ids = host.host_classes.pluck(:puppetclass_id)
ids += HostgroupClass.where(:hostgroup_id => hostgroup.path_ids).pluck(:puppetclass_id) if hostgroup
@puppetclass_ids = if Setting['remove_classes_not_in_environment']
EnvironmentClass.where(:environment_id => host.environment_id, :puppetclass_id => ids).
pluck('DISTINCT puppetclass_id')
else
ids
end
end
def classes
Puppetclass.where(:id => puppetclass_ids)
end
def possible_value_orders
class_parameters.select do |key|
# take only keys with actual values
key.lookup_values_count > 0 # we use counter cache, so its safe to make that query
end.map(&:path_elements).flatten(1).uniq
end
def values_hash options={}
values = {}
path2matches.each do |match|
LookupValue.where(:match => match).where(:lookup_key_id => class_parameters.map(&:id)).each do |value|
key_id = value.lookup_key_id
values[key_id] ||= {}
key = class_parameters.detect{|k| k.id == value.lookup_key_id }
name = key.to_s
element = match.split(LookupKey::EQ_DELM).first
next if options[:skip_fqdn] && element=="fqdn"
if values[key_id][name].nil?
values[key_id][name] = {:value => value.value, :element => element}
else
if key.path.index(element) < key.path.index(values[key_id][name][:element])
values[key_id][name] = {:value => value.value, :element => element}
end
end
end
end
values
end
def value_of_key(key, values)
if values[key.id] and values[key.id][key.to_s]
values[key.id][key.to_s][:value]
else
key.default_value
end
end
def hostgroup_matches
@hostgroup_matches ||= matches_for_hostgroup
end
def matches_for_hostgroup
matches = []
if hostgroup
path = hostgroup.to_label
while path.include?("/")
path = path[0..path.rindex("/")-1]
matches << "hostgroup#{LookupKey::EQ_DELM}#{path}"
end
end
matches
end
# Generate possible lookup values type matches to a given host
def path2matches
matches = []
possible_value_orders.each do |rule|
match = Array.wrap(rule).map do |element|
"#{element}#{LookupKey::EQ_DELM}#{attr_to_value(element)}"
end
matches << match.join(LookupKey::KEY_DELM)
hostgroup_matches.each do |hostgroup_match|
match[match.index{|m|m =~ /hostgroup\s*=/}]=hostgroup_match
matches << match.join(LookupKey::KEY_DELM)
end if Array.wrap(rule).include?("hostgroup") && Setting["host_group_matchers_inheritance"]
end
matches
end
# translates an element such as domain to its real value per host
# tries to find the host attribute first, parameters and then fallback to a puppet fact.
def attr_to_value element
# direct host attribute
return host.send(element) if host.respond_to?(element)
# host parameter
return host.host_params[element] if host.host_params.include?(element)
# fact attribute
if (fn = host.fact_names.first(:conditions => { :name => element }))
return FactValue.where(:host_id => host.id, :fact_name_id => fn.id).first.value
end
end
def path_elements path = nil
path.split.map do |paths|
paths.split(LookupKey::KEY_DELM).map do |element|
element
end
end
end
end
end
app/models/classification/class_param.rb
module Classification
class ClassParam < Base
def enc
key_hash = hashed_class_parameters
values = values_hash
klasses = {}
classes.each do |klass|
klasses[klass.name] ||= {}
if key_hash[klass.id]
key_hash[klass.id].each do |key|
klasses[klass.name][key.to_s] = value_of_key(key, values)
end
else
klasses[klass.name] = nil
end
end
klasses
end
protected
def class_parameters
@keys ||= LookupKey.includes(:environment_classes).parameters_for_class(puppetclass_ids, environment_id)
end
private
def hashed_class_parameters
h = {}
class_parameters.each do |key|
klass_id = key.environment_classes.first.puppetclass_id
h[klass_id] ||= []
h[klass_id] << key
end
h
end
end
end
app/models/classification/global_param.rb
module Classification
class GlobalParam < Base
def enc
values = values_hash
parameters = {}
class_parameters.each do |key|
parameters[key.to_s] = value_of_key(key, values)
end
parameters
end
protected
def class_parameters
@keys ||= LookupKey.global_parameters_for_class(puppetclass_ids)
end
end
end
app/models/concerns/authorization.rb
module Authorization
def self.included(base)
base.class_eval do
before_save :enforce_edit_permissions
before_destroy :enforce_destroy_permissions
before_create :enforce_create_permissions
end
end
# We must enforce the security model
def enforce_edit_permissions
enforce_permissions("edit") if enforce?
end
def enforce_destroy_permissions
enforce_permissions("destroy") if enforce?
end
def enforce_create_permissions
enforce_permissions("create") if enforce?
end
def enforce_permissions operation
# We get called again with the operation being set to create
return true if operation == "edit" and new_record?
klass = self.class.name.downcase
klasses = self.class.name.tableize
klasses.gsub!(/auth_source.*/, "authenticators")
klasses.gsub!(/common_parameters.*/, "global_variables")
klasses.gsub!(/lookup_key.*/, "external_variables")
klasses.gsub!(/lookup_value.*/, "external_variables")
return true if User.current and User.current.allowed_to?("#{operation}_#{klasses}".to_sym)
errors.add :base, _("You do not have permission to %{operation} this %{klass}") % { :operation => operation, :klass => klass }
@permission_failed = operation
false
end
# @return false or name of failed operation
def permission_failed?
return false unless @permission_failed
@permission_failed
end
private
def enforce?
return false if (User.current and User.current.admin?)
return true if defined?(Rake) and Rails.env == "test"
return false if defined?(Rake)
true
end
end
app/models/concerns/host_common.rb
require 'securerandom'
#Common methods between host and hostgroup
# mostly for template rendering consistency
module HostCommon
def self.included(base)
base.send :include, InstanceMethods
base.class_eval do
belongs_to :architecture
belongs_to :environment
belongs_to :operatingsystem
belongs_to :medium
belongs_to :ptable
belongs_to :puppet_proxy, :class_name => "SmartProxy"
belongs_to :puppet_ca_proxy, :class_name => "SmartProxy"
belongs_to :domain
belongs_to :subnet
before_save :check_puppet_ca_proxy_is_required?
has_many :lookup_values, :finder_sql => Proc.new { %Q{ SELECT lookup_values.* FROM lookup_values WHERE (lookup_values.match = '#{lookup_value_match}') } }, :dependent => :destroy
# See "def lookup_values_attributes=" under, for the implementation of accepts_nested_attributes_for :lookup_values
accepts_nested_attributes_for :lookup_values
# Replacement of accepts_nested_attributes_for :lookup_values,
# to work around the lack of `host_id` column in lookup_values.
def lookup_values_attributes= lookup_values_attributes
lookup_values_attributes.each_value do |attribute|
attr = attribute.dup
if attr.has_key? :id
lookup_value = lookup_values.find attr.delete(:id)
if lookup_value
mark_for_destruction = ActiveRecord::ConnectionAdapters::Column.value_to_boolean attr.delete(:_destroy)
lookup_value.attributes = attr
mark_for_destruction ? lookup_values.delete(lookup_value) : lookup_value.save!
end
elsif !ActiveRecord::ConnectionAdapters::Column.value_to_boolean attr.delete(:_destroy)
LookupValue.create(attr.merge(:match => lookup_value_match))
end
end
end
end
end
module InstanceMethods
# Returns a url pointing to boot file
def url_for_boot file
"#{os.medium_uri(self)}/#{os.url_for_boot(file)}"
end
def puppetca?
return false if self.respond_to?(:managed?) and !managed?
!!(puppet_ca_proxy and puppet_ca_proxy.url.present?)
end
# no need to store anything in the db if the entry is plain "puppet"
# If the system is using smart proxies and the user has run the smartproxy:migrate task
# then the puppetmaster functions handle smart proxy objects
def puppetmaster
puppet_proxy.to_s
end
def puppet_ca_server
puppet_ca_proxy.to_s
end
# If the host/hostgroup has a medium then use the path from there
# Else if the host/hostgroup's operatingsystem has only one media then use the image_path from that as this is automatically displayed when there is only one item
# Else we cannot provide a default and it is cut and paste time
def default_image_file
return "" unless operatingsystem and operatingsystem.supports_image
if medium
nfs_path = medium.try :image_path
if operatingsystem.try(:media) and operatingsystem.media.size == 1
nfs_path ||= operatingsystem.media.first.image_path
end
# We encode the hw_model into the image file name as not all Sparc flashes can contain all possible hw_models. The user can always
# edit it if required or use symlinks if they prefer.
hw_model = model.try :hardware_model if defined?(model_id)
operatingsystem.interpolate_medium_vars(nfs_path, architecture.name, operatingsystem) +\
"#{operatingsystem.file_prefix}.#{architecture}#{hw_model.empty? ? "" : "." + hw_model.downcase}.#{operatingsystem.image_extension}"
else
""
end
end
def image_file= file
# We only save a value into the image_file field if the value is not the default path, (which was placed in the entry when it was displayed,)
# and it is not a directory, (ends in /)
value = ( (default_image_file == file) or (file =~ /\/$/) or file == "") ? nil : file
write_attribute :image_file, value
end
def image_file
super || default_image_file
end
# make sure we store an encrypted copy of the password in the database
# this password can be use as is in a unix system
def root_pass=(pass)
p = pass.empty? ? nil : (pass.starts_with?('$') ? pass : pass.crypt("$1$#{SecureRandom.base64(6)}"))
write_attribute(:root_pass, p)
end
private
# fall back to our puppet proxy in case our puppet ca is not defined/used.
def check_puppet_ca_proxy_is_required?
return true if puppet_ca_proxy_id.present? or puppet_proxy_id.blank?
if puppet_proxy.features.include?(Feature.find_by_name "Puppet CA")
self.puppet_ca_proxy ||= puppet_proxy
end
rescue
true # we don't want to break anything, so just skipping.
end
end
end
app/models/concerns/host_template_helpers.rb
# These helpers are provided as convenience methods available to the writers of templates
# and are mixed in to Host
module HostTemplateHelpers
# Calculates the media's path in relation to the domain and convert host to an IP
def install_path
operatingsystem.interpolate_medium_vars(operatingsystem.media_path(medium, domain), architecture.name, operatingsystem)
end
# Calculates the jumpstart path in relation to the domain and convert host to an IP
def jumpstart_path
operatingsystem.jumpstart_path medium, domain
end
def multiboot
operatingsystem.pxe_prefix(architecture) + "-multiboot"
end
def miniroot
operatingsystem.initrd(architecture)
end
def media_path
operatingsystem.medium_uri(self)
end
#returns the URL for Foreman based on the required action
def foreman_url(action = "provision")
url_for :only_path => false, :controller => "/unattended",
:action => action,
:token => (@host.token.value unless @host.token.nil?)
end
attr_writer(:url_options)
# used by url_for to generate the path correctly
def url_options
url_options = (@url_options || {}).deep_dup()
url_options[:protocol] = "http://"
url_options[:host] = Setting[:foreman_url] if Setting[:foreman_url]
url_options
end
end
app/models/concerns/hostext/search.rb
module Hostext
module Search
def self.included(base)
base.class_eval do
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 => :name, :complete_value => true, :default_order => true
scoped_search :on => :last_report, :complete_value => true, :only_explicit => true
scoped_search :on => :ip, :complete_value => true
scoped_search :on => :comment, :complete_value => true
scoped_search :on => :enabled, :complete_value => {:true => true, :false => false}, :rename => :'status.enabled'
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 => :model, :on => :name, :complete_value => true, :rename => :model
scoped_search :in => :hostgroup, :on => :name, :complete_value => true, :rename => :hostgroup
scoped_search :in => :hostgroup, :on => :label, :complete_value => true, :rename => :hostgroup_fullname
scoped_search :in => :domain, :on => :name, :complete_value => true, :rename => :domain
scoped_search :in => :environment, :on => :name, :complete_value => true, :rename => :environment
scoped_search :in => :architecture, :on => :name, :complete_value => true, :rename => :architecture
scoped_search :in => :puppet_proxy, :on => :name, :complete_value => true, :rename => :puppetmaster
scoped_search :in => :puppet_ca_proxy, :on => :name, :complete_value => true, :rename => :puppet_ca
scoped_search :in => :compute_resource, :on => :name, :complete_value => true, :rename => :compute_resource
scoped_search :in => :image, :on => :name, :complete_value => true
scoped_search :in => :puppetclasses, :on => :name, :complete_value => true, :rename => :class, :only_explicit => true, :operators => ['= ', '~ '], :ext_method => :search_by_puppetclass
scoped_search :in => :fact_values, :on => :value, :in_key=> :fact_names, :on_key=> :name, :rename => :facts, :complete_value => true, :only_explicit => true
scoped_search :in => :search_parameters, :on => :value, :on_key=> :name, :complete_value => true, :rename => :params, :ext_method => :search_by_params, :only_explicit => true
scoped_search :in => :location, :on => :name, :rename => :location, :complete_value => true if SETTINGS[:locations_enabled]
scoped_search :in => :organization, :on => :name, :rename => :organization, :complete_value => true if SETTINGS[:organizations_enabled]
if SETTINGS[:unattended]
scoped_search :in => :subnet, :on => :network, :complete_value => true, :rename => :subnet
scoped_search :on => :mac, :complete_value => true
scoped_search :on => :uuid, :complete_value => true
scoped_search :on => :build, :complete_value => {:true => true, :false => false}
scoped_search :on => :installed_at, :complete_value => true, :only_explicit => true
scoped_search :in => :operatingsystem, :on => :name, :complete_value => true, :rename => :os
scoped_search :in => :operatingsystem, :on => :major, :complete_value => true, :rename => :os_major
scoped_search :in => :operatingsystem, :on => :minor, :complete_value => true, :rename => :os_minor
end
if SETTINGS[:login]
scoped_search :in => :search_users, :on => :login, :complete_value => true, :only_explicit => true, :rename => :'user.login', :operators => ['= ', '~ '], :ext_method => :search_by_user
scoped_search :in => :search_users, :on => :firstname, :complete_value => true, :only_explicit => true, :rename => :'user.firstname',:operators => ['= ', '~ '], :ext_method => :search_by_user
scoped_search :in => :search_users, :on => :lastname, :complete_value => true, :only_explicit => true, :rename => :'user.lastname', :operators => ['= ', '~ '], :ext_method => :search_by_user
scoped_search :in => :search_users, :on => :mail, :complete_value => true, :only_explicit => true, :rename => :'user.mail', :operators => ['= ', '~ '], :ext_method => :search_by_user
end
def self.search_by_user(key, operator, value)
key_name = User.connection.quote_column_name(key.sub(/^.*\./,''))
condition = sanitize_sql_for_conditions(["#{key_name} #{operator} ?", value_to_sql(operator, value)])
users = User.all(:conditions => condition)
hosts = users.map(&:hosts).flatten
opts = hosts.empty? ? "< 0" : "IN (#{hosts.map(&:id).join(',')})"
return {:conditions => " hosts.id #{opts} " }
end
def self.search_by_puppetclass(key, operator, value)
conditions = sanitize_sql_for_conditions(["puppetclasses.name #{operator} ?", value_to_sql(operator, value)])
hosts = Host.my_hosts.all(:conditions => conditions, :joins => :puppetclasses, :select => 'DISTINCT hosts.id').map(&:id)
host_groups = Hostgroup.all(:conditions => conditions, :joins => :puppetclasses, :select => 'DISTINCT hostgroups.id').map(&:id)
opts = ''
opts += "hosts.id IN(#{hosts.join(',')})" unless hosts.blank?
opts += " OR " unless hosts.blank? || host_groups.blank?
opts += "hostgroups.id IN(#{host_groups.join(',')})" unless host_groups.blank?
opts = "hosts.id < 0" if hosts.blank? && host_groups.blank?
return {:conditions => opts, :include => :hostgroup}
end
def self.search_by_params(key, operator, value)
key_name = key.sub(/^.*\./,'')
condition = sanitize_sql_for_conditions(["name = ? and value #{operator} ?", key_name, value_to_sql(operator, value)])
opts = {:conditions => condition, :order => :priority}
p = Parameter.all(opts)
return {:conditions => '1 = 0'} if p.blank?
max = p.first.priority
condition = sanitize_sql_for_conditions(["name = ? and NOT(value #{operator} ?) and priority > ?",key_name,value_to_sql(operator, value), max])
negate_opts = {:conditions => condition, :order => :priority}
n = Parameter.all(negate_opts)
conditions = param_conditions(p)
negate = param_conditions(n)
conditions += " AND " unless conditions.blank? || negate.blank?
conditions += " NOT(#{negate})" unless negate.blank?
return {:conditions => conditions}
end
private
def self.param_conditions(p)
conditions = []
p.each do |param|
case param.class.to_s
when 'CommonParameter'
# ignore
when 'DomainParameter'
conditions << "hosts.domain_id = #{param.reference_id}"
when 'OsParameter'
conditions << "hosts.operatingsystem_id = #{param.reference_id}"
when 'GroupParameter'
conditions << "hosts.hostgroup_id = #{param.reference_id}"
when 'HostParameter'
conditions << "hosts.id = #{param.reference_id}"
end
end
conditions.empty? ? [] : "( #{conditions.join(' OR ')} )"
end
def self.value_to_sql(operator, value)
return value if operator !~ /LIKE/i
return value.tr_s('%*', '%') if (value =~ /%|\*/)
return "%#{value}%"
end
end
end
end
end
app/models/concerns/orchestration.rb
require "proxy_api"
require 'orchestration/queue'
module Orchestration
def self.included(base)
base.send :include, InstanceMethods
base.class_eval do
attr_reader :old
# save handles both creation and update of hosts
before_save :on_save
after_commit :post_commit
after_destroy :on_destroy
end
end
module InstanceMethods
protected
def on_save
process :queue
end
def post_commit
process :post_queue
end
def on_destroy
errors.empty? ? process(:queue) : false
end
def rollback
raise ActiveRecord::Rollback
end
# log and add to errors
def failure msg, backtrace=nil, dest = :base
logger.warn(backtrace ? msg + backtrace.join("\n") : msg)
errors.add dest, msg
false
end
public
# we override this method in order to include checking the
# after validation callbacks status, as rails by default does
# not care about their return status.
def valid?(context = nil)
setup_clone
super
orchestration_errors?
end
def queue
@queue ||= Orchestration::Queue.new
end
def post_queue
@post_queue ||= Orchestration::Queue.new
end
def record_conflicts
@record_conflicts ||= []
end
private
def proxy_error e
e.respond_to?(:message) ? e.message : e
end
# Handles the actual queue
# takes care for running the tasks in order
# if any of them fail, it rollbacks all completed tasks
# in order not to keep any left overs in our proxies.
def process queue_name
return true if Rails.env == "test"
# queue is empty - nothing to do.
q = send(queue_name)
return if q.empty?
# process all pending tasks
q.pending.each do |task|
# if we have failures, we don't want to process any more tasks
break unless q.failed.empty?
task.status = "running"
update_cache
begin
task.status = execute({:action => task.action}) ? "completed" : "failed"
rescue Net::Conflict => e
task.status = "conflict"
record_conflicts << e
failure e.message, nil, :conflict
#TODO: This is not a real error, but at the moment the proxy / foreman lacks better handling
# of the error instead of explode.
rescue Net::LeaseConflict => e
task.status = "failed"
failure _("DHCP has a lease at %s") % e, e.backtrace
rescue RestClient::Exception => e
task.status = "failed"
failure _("%{task} task failed with the following error: %{e}") % { :task => task.name, :e => proxy_error(e) }, e.backtrace
rescue => e
task.status = "failed"
failure _("%{task} task failed with the following error: %{e}") % { :task => task.name, :e => e }, e.backtrace
end
end
update_cache
# if we have no failures - we are done
return true if q.failed.empty? and q.pending.empty? and q.conflict.empty? and orchestration_errors?
logger.warn "Rolling back due to a problem: #{q.failed + q.conflict}"
q.pending.each{ |task| task.status = "canceled" }
# handle errors
# we try to undo all completed operations and trigger a DB rollback
(q.completed + q.running).sort.reverse_each do |task|
begin
task.status = "rollbacked"
update_cache
execute({:action => task.action, :rollback => true})
rescue => e
# if the operation failed, we can just report upon it
failure _("Failed to perform rollback on %{task} - %{e}") % { :task => task.name, :e => e }
end
end
rollback
end
def execute opts = {}
obj, met = opts[:action]
rollback = opts[:rollback] || false
# at the moment, rollback are expected to replace set with del in the method name
if rollback
met = met.to_s
case met
when /set/
met.gsub!("set","del")
when /del/
met.gsub!("del","set")
else
raise "Dont know how to rollback #{met}"
end
met = met.to_sym
end
if obj.respond_to?(met,true)
return obj.send(met)
else
failure _("invalid method %s") % met
raise ::Foreman::Exception.new(N_("invalid method %s"), met)
end
end
# we keep the before update host object in order to compare changes
def setup_clone
return if new_record?
@old = dup
for key in (changed_attributes.keys - ["updated_at"])
@old.send "#{key}=", changed_attributes[key]
# At this point the old cached bindings may still be present so we force an AR association reload
# This logic may not work or be required if we switch to Rails 3
if (match = key.match(/^(.*)_id$/))
name = match[1].to_sym
next if name == :owner # This does not work for the owner association even from the console
self.send(name, true) if (send(name) and send(name).id != @attributes[key])
old.send(name, true) if (old.send(name) and old.send(name).id != old.attributes[key])
end
end
end
def orchestration_errors?
overwrite? ? errors.are_all_conflicts? : errors.empty?
end
def update_cache
Rails.cache.write(progress_report_id, (queue.all + post_queue.all).to_json, :expires_in => 5.minutes)
end
end
end
app/models/concerns/orchestration/compute.rb
module Orchestration::Compute
def self.included(base)
base.send :include, InstanceMethods
base.class_eval do
attr_accessor :compute_attributes, :vm, :provision_method
after_validation :validate_compute_provisioning, :queue_compute
before_destroy :queue_compute_destroy
end
end
module InstanceMethods
def compute?
compute_resource_id.present? and compute_attributes.present?
end
def compute_object
if uuid.present? and compute_resource_id.present?
compute_resource.find_vm_by_uuid(uuid) rescue nil
# we don't want the fact that we failed to fetch the information to break foreman
# this is mostly relevant when the orchestration had a failure, and later on in the ui we try to retrieve the server again.
# or when the server was removed not via foreman.
elsif compute_resource_id.present? && compute_attributes
compute_resource.new_vm compute_attributes
end
end
protected
def queue_compute
return unless compute? and errors.empty?
new_record? ? queue_compute_create : queue_compute_update
end
def queue_compute_create
queue.create(:name => _("Settings up compute instance %s") % self, :priority => 1,
:action => [self, :setCompute])
queue.create(:name => _("Acquiring IP address for %s") % self, :priority => 2,
:action => [self, :setComputeIP]) if compute_resource.provided_attributes.keys.include?(:ip)
queue.create(:name => _("Querying instance details for %s") % self, :priority => 3,
:action => [self, :setComputeDetails])
queue.create(:name => _("Power up compute instance %s") % self, :priority => 1000,
:action => [self, :setComputePowerUp]) if compute_attributes[:start] == '1'
end
def queue_compute_update
return unless compute_update_required?
logger.debug("Detected a change is required for Compute resource")
queue.create(:name => _("Compute resource update for %s") % old, :priority => 7,
:action => [self, :setComputeUpdate])
end
def queue_compute_destroy
return unless errors.empty? and compute_resource_id.present? and uuid
queue.create(:name => _("Removing compute instance %s") % self, :priority => 100,
:action => [self, :delCompute])
end
def setCompute
logger.info "Adding Compute instance for #{name}"
self.vm = compute_resource.create_vm compute_attributes.merge(:name => name)
rescue => e
failure _("Failed to create a compute %{compute_resource} instance %{name}: %{message}\n ") % { :compute_resource => compute_resource, :name => name, :message => e.message }, e.backtrace
end
def setComputeDetails
if vm
attrs = compute_resource.provided_attributes
normalize_addresses if attrs.keys.include?(:mac) or attrs.keys.include?(:ip)
attrs.each do |foreman_attr, fog_attr |
# we can't ensure uniqueness of #foreman_attr using normal rails validations as that gets in a later step in the process
# therefore we must validate its not used already in our db.
value = vm.send(fog_attr)
self.send("#{foreman_attr}=", value)
if value.blank? or (other_host = Host.send("find_by_#{foreman_attr}", value))
delCompute
return failure("#{foreman_attr} #{value} is already used by #{other_host}") if other_host
return failure("#{foreman_attr} value is blank!")
end
end
true
else
failure _("failed to save %s") % name
end
end
def delComputeDetails; end
def setComputeIP
attrs = compute_resource.provided_attributes
if attrs.keys.include?(:ip)
logger.info "waiting for instance to acquire ip address"
vm.wait_for { self.send(attrs[:ip]).present? }
end
rescue => e
failure _("Failed to get IP for %{name}: %{e}") % { :name => name, :e => e }, e.backtrace
end
def delComputeIP;end
def delCompute
logger.info "Removing Compute instance for #{name}"
compute_resource.destroy_vm uuid
rescue => e
failure _("Failed to destroy a compute %{compute_resource} instance %{name}: %{e}") % { :compute_resource => compute_resource, :name => name, :e => e }, e.backtrace
end
def setComputePowerUp
logger.info "Powering up Compute instance for #{name}"
compute_resource.start_vm uuid
rescue => e
failure _("Failed to power up a compute %{compute_resource} instance %{name}: %{e}") % { :compute_resource => compute_resource, :name => name, :e => e }, e.backtrace
end
def delComputePowerUp
logger.info "Powering down Compute instance for #{name}"
compute_resource.stop_vm uuid
rescue => e
failure _("Failed to stop compute %{compute_resource} instance %{name}: %{e}") % { :compute_resource => compute_resource, :name => name, :e => e }, e.backtrace
end
def setComputeUpdate
logger.info "Update Compute instance for #{name}"
compute_resource.save_vm uuid, compute_attributes
rescue => e
failure _("Failed to update a compute %{compute_resource} instance %{name}: %{e}") % { :compute_resource => compute_resource, :name => name, :e => e }, e.backtrace
end
def delComputeUpdate
logger.info "Undo Update Compute instance for #{name}"
compute_resource.save_vm uuid, old.compute_attributes
rescue => e
failure _("Failed to undo update compute %{compute_resource} instance %{name}: %{e}") % { :compute_resource => compute_resource, :name => name, :e => e }, e.backtrace
end
private
def compute_update_required?
return false unless compute_resource.supports_update?
old.compute_attributes = compute_resource.find_vm_by_uuid(uuid).attributes
compute_resource.update_required?(old.compute_attributes, compute_attributes.symbolize_keys)
end
def validate_compute_provisioning
return true if compute_attributes.nil?
image_uuid = compute_attributes[:image_id] || compute_attributes[:image_ref]
return true if image_uuid.blank?
img = Image.where(:uuid => image_uuid, :compute_resource_id => compute_resource_id).first
if img
self.image = img
else
failure(_("Selected image does not belong to %s") % compute_resource) and return false
end
end
end
end
app/models/concerns/orchestration/dhcp.rb
module Orchestration::DHCP
def self.included(base)
base.send :include, InstanceMethods
base.class_eval do
after_validation :queue_dhcp
before_destroy :queue_dhcp_destroy
validate :ip_belongs_to_subnet?
end
end
module InstanceMethods
def dhcp?
name.present? and ip.present? and !subnet.nil? and subnet.dhcp? and managed? and capabilities.include?(:build)
end
def dhcp_record
return unless dhcp? or @dhcp_record
@dhcp_record ||= jumpstart? ? Net::DHCP::SparcRecord.new(dhcp_attrs) : Net::DHCP::Record.new(dhcp_attrs)
end
protected
def set_dhcp
dhcp_record.create
end
def set_dhcp_conflicts
dhcp_record.conflicts.each{|conflict| conflict.create}
end
def del_dhcp
dhcp_record.destroy
end
def del_dhcp_conflicts
dhcp_record.conflicts.each{|conflict| conflict.destroy}
end
# where are we booting from
def boot_server
# if we don't manage tftp at all, we dont create a next-server entry.
return unless tftp?
# first try to ask our TFTP server for its boot server
bs = tftp.bootServer
# if that failed, trying to guess out tftp next server based on the smart proxy hostname
bs ||= URI.parse(subnet.tftp.url).host
# now convert it into an ip address (see http://theforeman.org/issues/show/1381)
return to_ip_address(bs) if bs.present?
failure _("Unable to determine the host's boot server. The DHCP smart proxy failed to provide this information and this subnet is not provided with TFTP services.")
rescue => e
failure _("failed to detect boot server: %s") % e
end
private
# returns a hash of dhcp record settings
def dhcp_attrs
return unless dhcp?
dhcp_attr = { :name => name, :filename => operatingsystem.boot_filename(self),
:ip => ip, :mac => mac, :hostname => name, :proxy => subnet.dhcp_proxy,
:network => subnet.network, :nextServer => boot_server }
if jumpstart?
jumpstart_arguments = os.jumpstart_params self, model.vendor_class
dhcp_attr.merge! jumpstart_arguments unless jumpstart_arguments.empty?
end
dhcp_attr
end
def queue_dhcp
return unless (dhcp? or (old and old.dhcp?)) and orchestration_errors?
queue_remove_dhcp_conflicts if dhcp_conflict_detected?
new_record? ? queue_dhcp_create : queue_dhcp_update
end
def queue_dhcp_create
logger.debug "Scheduling new DHCP reservations for #{self}"
queue.create(:name => _("Create DHCP Settings for %s") % self, :priority => 10,
:action => [self, :set_dhcp]) if dhcp?
end
def queue_dhcp_update
if dhcp_update_required?
logger.debug("Detected a changed required for DHCP record")
queue.create(:name => _("Remove DHCP Settings for %s") % old, :priority => 5,
:action => [old, :del_dhcp]) if old.dhcp?
queue.create(:name => _("Create DHCP Settings for %s") % self, :priority => 9,
:action => [self, :set_dhcp]) if dhcp?
end
end
# do we need to update our dhcp reservations
def dhcp_update_required?
# IP Address / name changed
return true if ((old.ip != ip) or (old.name != name) or (old.mac != mac) or (old.subnet != subnet))
# Handle jumpstart
#TODO, abstract this way once interfaces are fully used
if self.kind_of?(Host::Base) and jumpstart?
if !old.build? or (old.medium != medium or old.arch != arch) or
(os and old.os and (old.os.name != os.name or old.os != os))
return true
end
end
false
end
def queue_dhcp_destroy
return unless dhcp? and errors.empty?
queue.create(:name => _("Remove DHCP Settings for %s") % self, :priority => 5,
:action => [self, :del_dhcp])
true
end
def queue_remove_dhcp_conflicts
return unless dhcp? and errors.any? and errors.are_all_conflicts?
return unless overwrite?
logger.debug "Scheduling DHCP conflicts removal"
queue.create(:name => _("DHCP conflicts removal for %s") % self, :priority => 5,
:action => [self, :del_dhcp_conflicts]) if dhcp_record and dhcp_record.conflicting?
end
def ip_belongs_to_subnet?
return if subnet.nil? or ip.nil?
return unless dhcp?
unless subnet.contains? ip
errors.add(:ip, _("Does not match selected Subnet"))
return false
end
rescue
# probably an invalid ip / subnet were entered
# we let other validations handle that
end
def dhcp_conflict_detected?
# we can't do any dhcp based validations when our MAC address is defined afterwards (e.g. in vm creation)
return false if mac.blank? or name.blank?
# This is an expensive operation and we will only do it if the DNS validation failed. This will ensure
# that we report on both DNS and DHCP conflicts when we offer to remove collisions. It retrieves and
# caches the conflicting records so we must always do it when overwriting
return false unless (errors.any? and errors.are_all_conflicts?) or overwrite?
return false unless dhcp?
status = true
status = failure(_("DHCP records %s already exists") % dhcp_record.conflicts.to_sentence, nil, :conflict) if dhcp_record and dhcp_record.conflicting?
overwrite? ? errors.are_all_conflicts? : status
end
end
end
app/models/concerns/orchestration/dns.rb
module Orchestration::DNS
def self.included(base)
base.send :include, InstanceMethods
base.class_eval do
after_validation :dns_conflict_detected?, :queue_dns
before_destroy :queue_dns_destroy
end
end
module InstanceMethods
def dns?
name.present? and ip_available? and !domain.nil? and !domain.proxy.nil? and managed?
end
def reverse_dns?
name.present? and ip_available? and !subnet.nil? and subnet.dns? and managed?
end
def dns_a_record
return unless dns? or @dns_a_record
@dns_a_record ||= Net::DNS::ARecord.new dns_record_attrs
end
def dns_ptr_record
return unless reverse_dns? or @dns_ptr_record
@dns_ptr_record ||= Net::DNS::PTRRecord.new reverse_dns_record_attrs
end
protected
def set_dns_a_record
dns_a_record.create
end
def set_conflicting_dns_a_record
dns_a_record.conflicts.each { |c| c.create }
end
def set_dns_ptr_record
dns_ptr_record.create
end
def set_conflicting_dns_ptr_record
dns_ptr_record.conflicts.each { |c| c.create }
end
def del_dns_a_record
dns_a_record.destroy
end
def del_conflicting_dns_a_record
dns_a_record.conflicts.each { |c| c.destroy }
end
def del_dns_ptr_record
dns_ptr_record.destroy
end
def del_conflicting_dns_ptr_record
dns_ptr_record.conflicts.each { |c| c.destroy }
end
private
def dns_record_attrs
{ :hostname => name, :ip => ip, :resolver => domain.resolver, :proxy => domain.proxy }
end
def reverse_dns_record_attrs
{ :hostname => name, :ip => ip, :proxy => subnet.dns_proxy }
end
def queue_dns
return unless (dns? or reverse_dns?) and errors.empty?
queue_remove_dns_conflicts if overwrite?
new_record? ? queue_dns_create : queue_dns_update
end
def queue_dns_create
logger.debug "Scheduling new DNS entries"
queue.create(:name => _("Create DNS record for %s") % self, :priority => 10,
:action => [self, :set_dns_a_record]) if dns?
queue.create(:name => _("Create Reverse DNS record for %s") % self, :priority => 10,
:action => [self, :set_dns_ptr_record]) if reverse_dns?
end
def queue_dns_update
if old.ip != ip or old.name != name
queue.create(:name => _("Remove DNS record for %s") % old, :priority => 9,
:action => [old, :del_dns_a_record]) if old.dns?
queue.create(:name => _("Remove Reverse DNS record for %s") % old, :priority => 9,
:action => [old, :del_dns_ptr_record]) if old.reverse_dns?
queue_dns_create
end
end
def queue_dns_destroy
return unless errors.empty?
queue.create(:name => _("Remove DNS record for %s") % self, :priority => 1,
:action => [self, :del_dns_a_record]) if dns?
queue.create(:name => _("Remove Reverse DNS record for %s") % self, :priority => 1,
:action => [self, :del_dns_ptr_record]) if reverse_dns?
end
def queue_remove_dns_conflicts
return unless errors.empty?
return unless overwrite?
logger.debug "Scheduling DNS conflict removal"
queue.create(:name => _("Remove conflicting DNS record for %s") % self, :priority => 0,
:action => [self, :del_conflicting_dns_a_record]) if dns? and dns_a_record and dns_a_record.conflicting?
queue.create(:name => _("Remove conflicting Reverse DNS record for %s") % self, :priority => 0,
:action => [self, :del_conflicting_dns_ptr_record]) if reverse_dns? and dns_ptr_record and dns_ptr_record.conflicting?
end
def dns_conflict_detected?
return false if ip.blank? or name.blank?
# can't validate anything if dont have an ip-address yet
return false unless require_ip_validation?
# we should only alert on conflicts if overwrite mode is off
return false if overwrite?
status = true
status = failure(_("DNS A Records %s already exists") % dns_a_record.conflicts.to_sentence, nil, :conflict) if dns? and dns_a_record and dns_a_record.conflicting?
status &= failure(_("DNS PTR Records %s already exists") % dns_ptr_record.conflicts.to_sentence, nil, :conflict) if reverse_dns? and dns_ptr_record and dns_ptr_record.conflicting?
status
end
def ip_available?
ip.present? || (capabilities.include?(:image) && compute_resource.provided_attributes.keys.include?(:ip))
end
end
end
app/models/concerns/orchestration/puppetca.rb
module Orchestration::Puppetca
def self.included(base)
base.send :include, InstanceMethods
base.class_eval do
attr_reader :puppetca
after_validation :initialize_puppetca, :queue_puppetca
before_destroy :initialize_puppetca, :queue_puppetca_destroy unless Rails.env == "test"
end
end
module InstanceMethods
protected
def initialize_puppetca
return unless puppetca?
return unless Setting[:manage_puppetca]
@puppetca = ProxyAPI::Puppetca.new :url => puppet_ca_proxy.url
true
rescue => e
failure _("Failed to initialize the PuppetCA proxy: %s") % e
end
# Removes the host's puppet certificate from the puppetmaster's CA
def delCertificate
logger.info "Remove puppet certificate for #{name}"
puppetca.del_certificate certname
rescue => e
failure _("Failed to remove %{name}'s puppet certificate: %{e}") % { :name => name, :e => proxy_error(e) }
end
# Empty method for rollbacks - maybe in the future we would support creating the certificates directly
def setCertificate; end
# Adds the host's name to the autosign.conf file
def setAutosign
logger.info "Adding autosign entry for #{name}"
puppetca.set_autosign certname
rescue => e
failure _("Failed to add %{name} to autosign file: %{e}") % { :name => name, :e => proxy_error(e) }
end
# Removes the host's name from the autosign.conf file
def delAutosign
logger.info "Delete the autosign entry for #{name}"
puppetca.del_autosign certname
rescue => e
failure _("Failed to remove %{self} from the autosign file: %{e}") % { :self => self, :e => proxy_error(e) }
end
private
def queue_puppetca
return unless puppetca? and errors.empty?
return unless Setting[:manage_puppetca]
new_record? ? queue_puppetca_create : queue_puppetca_update
end
# we don't perform any actions upon create
# PuppetCA is set only when a provisioning script (such as a kickstart) is being requested.
def queue_puppetca_create; end
def queue_puppetca_update
# Host has been built --> remove auto sign
if old.build? and !build?
queue.create(:name => _("Delete autosign entry for %s") % self, :priority => 50,
:action => [self, :delAutosign])
end
end
def queue_puppetca_destroy
return unless puppetca? and errors.empty?
return unless Setting[:manage_puppetca]
queue.create(:name => _("Delete PuppetCA certificates for %s") % self, :priority => 50,
:action => [self, :delCertificate])
queue.create(:name => _("Delete PuppetCA certificates for %s") % self, :priority => 55,
:action => [self, :delAutosign])
end
end
end
app/models/concerns/orchestration/ssh_provision.rb
module Orchestration::SSHProvision
def self.included(base)
base.send :include, InstanceMethods
base.class_eval do
after_validation :validate_ssh_provisioning, :queue_ssh_provision
attr_accessor :template_file, :client
... This diff was truncated because it exceeds the maximum size that can be displayed.

Also available in: Unified diff