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