app/models/job_invocation_composer.rb
class JobInvocationComposer
class JobTemplateNotFound < StandardError; end
class FeatureNotFound < StandardError; end
class UiParams
attr_reader :ui_params
def initialize(ui_params)
@ui_params = ui_params
end
def params
{ :job_category => job_invocation_base[:job_category],
:targeting => targeting(ui_params.fetch(:targeting, {})),
:triggering => triggering,
:host_ids => ui_params[:host_ids],
:remote_execution_feature_id => job_invocation_base[:remote_execution_feature_id],
:description_format => job_invocation_base[:description_format],
:ssh_user => blank_to_nil(job_invocation_base[:ssh_user]),
:password => blank_to_nil(job_invocation_base[:password]),
:key_passphrase => blank_to_nil(job_invocation_base[:key_passphrase]),
:effective_user_password => blank_to_nil(job_invocation_base[:effective_user_password]),
:concurrency_control => concurrency_control_params,
:execution_timeout_interval => execution_timeout_interval,
:time_to_pickup => job_invocation_base[:time_to_pickup],
:template_invocations => template_invocations_params }.with_indifferent_access
end
def job_invocation_base
ui_params.fetch(:job_invocation, {})
end
def providers_base
job_invocation_base.fetch(:providers, {})
end
def execution_timeout_interval
providers_base.values.map do |provider|
id = provider[:job_template_id]
provider.fetch(:job_templates, {}).fetch(id, {})[:execution_timeout_interval]
end.first
end
def blank_to_nil(thing)
thing.presence
end
# TODO: Fix this comment
# parses params to get job templates in form of id => attributes for selected job templates, e.g.
# {
# "459" => {},
# "454" => {
# "input_values" => {
# "2" => {
# "value" => ""
# },
# "5" => {
# "value" => ""
# }
# }
# }
# }
def template_invocations_params
providers_base.values.map do |template_params|
template_base = template_params.fetch(:job_templates, {}).fetch(template_params[:job_template_id], {}).dup.with_indifferent_access
template_base[:template_id] = template_params[:job_template_id]
input_values_params = template_base.fetch(:input_values, {})
template_base[:input_values] = input_values_params.map do |id, values|
values.merge(:template_input_id => id)
end
provider_values_params = template_base.fetch(:provider_input_values, {})
template_base[:provider_input_values] = provider_values_params.map do |key, hash|
{ :name => key, :value => hash[:value] }
end
template_base
end
end
def concurrency_control_params
{
:level => job_invocation_base[:concurrency_level],
}
end
def triggering
return {} unless ui_params.key?(:triggering)
trig = ui_params[:triggering]
keys = (1..5).map { |i| "end_time(#{i}i)" }
values = trig.fetch(:end_time, {}).values_at(*keys)
return trig if values.any?(&:nil?)
trig.merge(:end_time => Time.local(*values))
end
def targeting(targeting_params)
targeting_params.merge(:user_id => User.current.id)
end
end
class ApiParams
attr_reader :api_params
def initialize(api_params)
@api_params = api_params
if api_params[:feature]
# set a default targeting type for backward compatibility
# when `for_feature` was used by the API it automatically set a default
api_params[:targeting_type] = Targeting::STATIC_TYPE
end
if api_params[:search_query].blank? && api_params[:host_ids].present?
translator = HostIdsTranslator.new(api_params[:host_ids])
api_params[:search_query] = translator.scoped_search
end
end
def params
{ :job_category => template.job_category,
:targeting => targeting_params,
:triggering => triggering_params,
:description_format => api_params[:description_format],
:ssh_user => api_params[:ssh_user],
:password => api_params[:password],
:remote_execution_feature_id => remote_execution_feature_id,
:effective_user_password => api_params[:effective_user_password],
:key_passphrase => api_params[:key_passphrase],
:concurrency_control => concurrency_control_params,
:execution_timeout_interval => api_params[:execution_timeout_interval] || template.execution_timeout_interval,
:time_to_pickup => api_params[:time_to_pickup],
:template_invocations => template_invocations_params }.with_indifferent_access
end
def remote_execution_feature_id
feature&.id || api_params[:remote_execution_feature_id]
end
def targeting_params
raise ::Foreman::Exception, _('Cannot specify both bookmark_id and search_query') if api_params[:bookmark_id] && api_params[:search_query]
api_params.slice(:targeting_type, :bookmark_id, :search_query, :randomized_ordering).merge(:user_id => User.current.id)
end
def triggering_params
if api_params[:recurrence].present? && api_params[:scheduling].present?
recurring_mode_params.merge :start_at_raw => format_datetime(api_params[:scheduling][:start_at])
elsif api_params[:recurrence].present? && api_params[:scheduling].empty?
recurring_mode_params
elsif api_params[:recurrence].empty? && api_params[:scheduling].present?
{
:mode => :future,
:start_at_raw => format_datetime(api_params[:scheduling][:start_at]),
:start_before_raw => format_datetime(api_params[:scheduling][:start_before]),
:end_time_limited => api_params[:scheduling][:start_before] ? true : false,
}
else
{}
end
end
def recurring_mode_params
{
:mode => :recurring,
:cronline => api_params[:recurrence][:cron_line],
:end_time => format_datetime(api_params[:recurrence][:end_time]),
:input_type => :cronline,
:max_iteration => api_params[:recurrence][:max_iteration],
:purpose => api_params[:recurrence][:purpose],
}
end
def concurrency_control_params
{
:level => api_params.fetch(:concurrency_control, {})[:concurrency_level],
}
end
def template_invocations_params
template_invocation_params = { :template_id => template.id, :effective_user => api_params[:effective_user] }
template_invocation_params[:provider_input_values] = filter_provider_inputs api_params
template_invocation_params[:input_values] = api_params.fetch(:inputs, {}).to_h.map do |name, value|
input = template.template_inputs_with_foreign.find { |i| i.name == name }
unless input
raise ::Foreman::Exception, _('Unknown input %{input_name} for template %{template_name}') %
{ :input_name => name, :template_name => template.name }
end
{ :template_input_id => input.id, :value => value }
end
[template_invocation_params]
end
def filter_provider_inputs(api_params)
return [] if template.provider.provider_input_namespace.empty?
inputs = api_params[template.provider.provider_input_namespace].to_h
provider_input_names = template.provider.provider_inputs.map(&:name)
inputs.select { |key, value| provider_input_names.include? key }.map { |key, value| { :name => key, :value => value } }
end
def feature
@feature ||= RemoteExecutionFeature.feature(api_params[:feature]) if api_params[:feature]
rescue => e
raise(FeatureNotFound, e.message)
end
def job_template_id
feature&.job_template_id || api_params[:job_template_id]
end
def template
@template ||= JobTemplate.authorized(:view_job_templates).find(job_template_id)
rescue ActiveRecord::RecordNotFound
raise(JobTemplateNotFound, _("Template with id '%{id}' was not found") % { id: job_template_id })
end
private
def format_datetime(datetime)
return datetime if datetime.blank?
Time.zone.parse(datetime).strftime('%Y-%m-%d %H:%M')
end
end
class ParamsFromJobInvocation
attr_reader :job_invocation
def initialize(job_invocation, params = {})
@job_invocation = job_invocation
if params[:host_ids]
@host_ids = params[:host_ids]
elsif params[:failed_only]
@host_ids = job_invocation.failed_host_ids
elsif params[:succeeded_only]
@host_ids = job_invocation.succeeded_host_ids
end
end
def params
{ :job_category => job_invocation.job_category,
:targeting => targeting_params,
:triggering => triggering_params,
:ssh_user => job_invocation.ssh_user,
:description_format => job_invocation.description_format,
:concurrency_control => concurrency_control_params,
:execution_timeout_interval => job_invocation.execution_timeout_interval,
:remote_execution_feature_id => job_invocation.remote_execution_feature_id,
:template_invocations => template_invocations_params,
:time_to_pickup => job_invocation.time_to_pickup,
:reruns => job_invocation.id }.with_indifferent_access
end
private
def concurrency_control_params
{
:level => job_invocation.concurrency_level,
}
end
def targeting_params
base = { :user_id => User.current.id }
if @host_ids
search_query = @host_ids.empty? ? 'name ^ ()' : Targeting.build_query_from_hosts(@host_ids)
base.merge(:search_query => search_query).merge(job_invocation.targeting.attributes.slice('targeting_type', 'randomized_ordering'))
else
base.merge job_invocation.targeting.attributes.slice('search_query', 'bookmark_id', 'targeting_type', 'randomized_ordering')
end
end
def template_invocations_params
job_invocation.pattern_template_invocations.map do |template_invocation|
params = template_invocation.attributes.slice('template_id', 'effective_user')
params['input_values'] = template_invocation.input_values.map { |v| v.attributes.slice('template_input_id', 'value') }
params['provider_input_values'] = template_invocation.provider_input_values.map { |v| v.attributes.slice('name', 'value') }
params
end
end
def triggering_params
ForemanTasks::Triggering.new_from_params.attributes.slice('mode', 'start_at', 'start_before')
end
end
class HostIdsTranslator
attr_reader :bookmark, :hosts, :scoped_search, :host_ids
def initialize(input)
case input
when Bookmark
@bookmark = input
when Host::Base
@hosts = [input]
when Array
@hosts = input.map do |id|
Host::Managed.authorized.friendly.find(id)
end
when String
@scoped_search = input
else
@hosts = input
end
@scoped_search ||= Targeting.build_query_from_hosts(hosts.map(&:id)) if @hosts
end
end
class ParamsForFeature
attr_reader :feature_label, :feature, :provided_inputs
def initialize(feature_label, hosts, provided_inputs = {})
@feature = RemoteExecutionFeature.feature!(feature_label)
@provided_inputs = provided_inputs
translator = HostIdsTranslator.new(hosts)
@host_bookmark = translator.bookmark
@host_scoped_search = translator.scoped_search
end
def params
{ :job_category => job_template.job_category,
:targeting => targeting_params,
:triggering => {},
:concurrency_control => {},
:remote_execution_feature_id => @feature.id,
:template_invocations => template_invocations_params }.with_indifferent_access
end
private
def targeting_params
ret = {}
ret['targeting_type'] = Targeting::STATIC_TYPE
ret['search_query'] = @host_scoped_search if @host_scoped_search
ret['bookmark_id'] = @host_bookmark.id if @host_bookmark
ret['user_id'] = User.current.id
ret
end
def template_invocations_params
[ { 'template_id' => job_template.id,
'input_values' => input_values_params } ]
end
def input_values_params
return {} if @provided_inputs.blank?
@provided_inputs.map do |key, value|
input = job_template.template_inputs_with_foreign.find { |i| i.name == key.to_s }
unless input
raise Foreman::Exception.new(N_('Feature input %{input_name} not defined in template %{template_name}'),
:input_name => key, :template_name => job_template.name)
end
{ 'template_input_id' => input.id, 'value' => value }
end
end
def job_template
unless feature.job_template
raise Foreman::Exception.new(N_('No template mapped to feature %{feature_name}'),
:feature_name => feature.name)
end
template = JobTemplate.authorized(:view_job_templates).find_by(id: feature.job_template_id)
unless template
raise Foreman::Exception.new(N_('The template %{template_name} mapped to feature %{feature_name} is not accessible by the user'),
:template_name => template.name,
:feature_name => feature.name)
end
template
end
end
attr_accessor :params, :job_invocation, :host_ids, :search_query
attr_reader :reruns
delegate :job_category, :remote_execution_feature_id, :pattern_template_invocations, :template_invocations, :targeting, :triggering, :to => :job_invocation
def initialize(params, set_defaults = false)
@params = params
@set_defaults = set_defaults
@job_invocation = JobInvocation.new
@job_invocation.task_group = JobInvocationTaskGroup.new
@reruns = params[:reruns]
compose
@host_ids = validate_host_ids(params[:host_ids])
@search_query = job_invocation.targeting.search_query if job_invocation.targeting.bookmark_id.blank?
end
def self.from_job_invocation(job_invocation, params = {})
self.new(ParamsFromJobInvocation.new(job_invocation, params).params)
end
def self.from_ui_params(ui_params)
self.new(UiParams.new(ui_params).params, true)
end
def self.from_api_params(api_params)
self.new(ApiParams.new(api_params).params)
end
def self.for_feature(feature_label, hosts, provided_inputs = {})
self.new(ParamsForFeature.new(feature_label, hosts, provided_inputs).params)
end
def compose
job_invocation.job_category = validate_job_category(params[:job_category])
job_invocation.job_category ||= resolve_job_category(available_job_categories.first) { |template| template.job_category } if @set_defaults
job_invocation.remote_execution_feature_id = params[:remote_execution_feature_id]
job_invocation.targeting = build_targeting
job_invocation.triggering = build_triggering
job_invocation.pattern_template_invocations = build_template_invocations
job_invocation.description_format = params[:description_format]
job_invocation.concurrency_level = params[:concurrency_control][:level].to_i if params[:concurrency_control][:level].present?
job_invocation.execution_timeout_interval = params[:execution_timeout_interval]
job_invocation.password = params[:password]
job_invocation.key_passphrase = params[:key_passphrase]
job_invocation.effective_user_password = params[:effective_user_password]
job_invocation.ssh_user = params[:ssh_user]
job_invocation.time_to_pickup = params[:time_to_pickup]
if @reruns && job_invocation.targeting.static?
job_invocation.targeting.assign_host_ids(JobInvocation.find(@reruns).targeting.host_ids)
job_invocation.targeting.mark_resolved!
end
job_invocation.job_category = nil unless rerun_possible?
self
end
def trigger(raise_on_error = false)
generate_description
if raise_on_error
save!
else
return false unless save
end
triggering.trigger(::Actions::RemoteExecution::RunHostsJob, job_invocation)
end
def trigger!
trigger(true)
end
def valid?
unless triggering.valid?
job_invocation.errors.add(:triggering, 'is invalid')
return false
end
targeting.valid? & job_invocation.valid? & !pattern_template_invocations.map(&:valid?).include?(false)
end
def save
valid? && job_invocation.save
end
def save!
if valid?
job_invocation.save!
else
raise job_invocation.flattened_validation_exception
end
end
def available_templates
JobTemplate.authorized(:view_job_templates).where(:snippet => false)
end
def available_templates_for(job_category)
available_templates.where(:job_category => job_category)
end
def available_job_categories
available_templates.reorder(:job_category).group(:job_category).pluck(:job_category)
end
def available_provider_types
available_templates_for(job_category).reorder(:provider_type).group(:provider_type).pluck(:provider_type)
end
def available_template_inputs
TemplateInput.where(:template_id => job_template_ids.empty? ? available_templates_for(job_category).map(&:id) : job_template_ids)
end
def needs_provider_type_selection?
available_provider_types.size > 1
end
def displayed_provider_types
# TODO available_provider_types based on targets
available_provider_types
end
def templates_for_provider(provider_type)
available_templates_for(job_category).select { |t| t.provider_type == provider_type }
end
def selected_job_templates
available_templates_for(job_category).where(:id => job_template_ids)
end
def preselected_template_for_provider(provider_type)
(templates_for_provider(provider_type) & selected_job_templates).first
end
def resolve_job_category(default_category)
resolve_for_composer(default_category) { |form_template| form_template.job_category }
end
def resolve_job_template(provider_templates)
resolve_for_composer(provider_templates.first) do |form_template|
provider_templates.include?(form_template) ? form_template : provider_templates.first
end
end
def displayed_search_query
if @search_query.present?
@search_query
elsif host_ids.present?
Targeting.build_query_from_hosts(host_ids)
elsif targeting.bookmark_id
if (bookmark = available_bookmarks.find_by(:id => targeting.bookmark_id))
bookmark.query
else
''
end
else
''
end
end
def available_bookmarks
Bookmark.my_bookmarks.where(:controller => ['hosts', 'dashboard'])
end
def targeted_hosts
if displayed_search_query.blank?
Host.where('1 = 0')
else
Host.execution_scope.authorized(Targeting::RESOLVE_PERMISSION, Host).search_for(displayed_search_query)
end
end
def targeted_hosts_count
targeted_hosts.count
rescue
0
end
def template_invocation(job_template)
pattern_template_invocations.find { |invocation| invocation.template == job_template }
end
def input_value_for(input)
invocations = pattern_template_invocations
default = TemplateInvocationInputValue.new(:template_input_id => input.id, :value => input.default)
invocations.map(&:input_values).flatten.detect { |iv| iv.template_input_id == input.id } || default
end
def job_template_ids
job_invocation.pattern_template_invocations.map(&:template_id)
end
def rerun_possible?
!(reruns && job_invocation.pattern_template_invocations.empty?)
end
private
# builds input values for a given templates id based on params
# omits inputs that belongs to unavailable templates
def build_input_values_for(template_invocation, job_template_base)
template_invocation.input_values = template_invocation.template.template_inputs_with_foreign.map do |input|
attributes = job_template_base.fetch('input_values', {}).find { |i| i[:template_input_id].to_s == input.id.to_s }
attributes = { "template_input_id" => input.id, "value" => input.default } if attributes.nil? && input.default
attributes ? input.template_invocation_input_values.build(attributes) : nil
end.compact
template_invocation.provider_input_values.build job_template_base.fetch('provider_input_values', [])
end
def build_targeting
# if bookmark was used we compare it to search query,
# when it's the same, we delete the query since it is used from bookmark
# when no bookmark is set we store the query
bookmark_id = params[:targeting][:bookmark_id]
bookmark = available_bookmarks.find_by(:id => bookmark_id)
query = params[:targeting][:search_query]
if bookmark.present? && query.present?
if query.strip == bookmark.query.strip
query = nil
else
bookmark_id = nil
end
elsif query.present?
query = params[:targeting][:search_query]
bookmark_id = nil
end
Targeting.new(
:bookmark_id => bookmark_id,
:targeting_type => params[:targeting][:targeting_type],
:search_query => query,
:randomized_ordering => params[:targeting][:randomized_ordering]
) { |t| t.user_id = params[:targeting][:user_id] }
end
def build_triggering
::ForemanTasks::Triggering.new_from_params(params[:triggering])
end
def build_template_invocations
valid_template_ids = validate_job_template_ids(params[:template_invocations].map { |t| t[:template_id] })
params[:template_invocations].select { |t| valid_template_ids.include?(t[:template_id].to_i) }.map do |template_invocation_params|
template_invocation = job_invocation.pattern_template_invocations.build(:template_id => template_invocation_params[:template_id],
:effective_user => build_effective_user(template_invocation_params))
build_input_values_for(template_invocation, template_invocation_params)
template_invocation
end
end
def generate_description
unless job_invocation.description_format
template = job_invocation.pattern_template_invocations.first.try(:template)
job_invocation.description_format = template.generate_description_format if template
end
job_invocation.generate_description if job_invocation.description.blank?
end
def build_effective_user(template_invocation_params)
job_template = available_templates.find(template_invocation_params[:template_id])
if job_template.effective_user.overridable? && template_invocation_params[:effective_user].present?
template_invocation_params[:effective_user]
else
job_template.effective_user.compute_value
end
end
# returns nil if user can't see any job template with such name
# existing job_category string otherwise
def validate_job_category(name)
available_job_categories.include?(name) ? name : nil
end
def validate_job_template_ids(ids)
available_templates_for(job_category).where(:id => ids).pluck(:id)
end
def validate_host_ids(ids)
Host.authorized(Targeting::RESOLVE_PERMISSION, Host).where(:id => ids).pluck(:id)
end
def resolve_for_composer(default_value, &block)
setting_value = Setting['remote_execution_form_job_template']
return default_value unless setting_value
form_template = JobTemplate.authorized(:view_job_templates).find_by :name => setting_value
return default_value unless form_template
if block_given?
yield form_template
else
form_template
end
end
end