module Actions class ProxyAction < Base include ::Dynflow::Action::Cancellable include ::Dynflow::Action::Timeouts class CallbackData attr_reader :data def initialize(data) @data = data end end def plan(proxy, options) options[:connection_options] ||= {} default_connection_options.each { |key, value| options[:connection_options][key] ||= value } plan_self(options.merge(:proxy_url => proxy.url)) end def run(event = nil) with_connection_error_handling(event) do |event| case event when nil if output[:proxy_task_id] on_resume else trigger_proxy_task end suspend when ::Dynflow::Action::Skip # do nothing when ::Dynflow::Action::Cancellable::Cancel cancel_proxy_task when CallbackData on_data(event.data) when ::Dynflow::Action::Timeouts::Timeout check_task_status else raise "Unexpected event #{event.inspect}" end end end def trigger_proxy_task suspend do |_suspended_action| set_timeout! unless timeout_set? response = proxy.trigger_task(proxy_action_name, input.merge(:callback => { :task_id => task.id, :step_id => run_step_id })) output[:proxy_task_id] = response["task_id"] end end def check_task_status if output[:proxy_task_id] response = proxy.status_of_task(output[:proxy_task_id]) if %w(stopped paused).include? response['state'] if response['result'] == 'error' raise ::Foreman::Exception, _("The smart proxy task %s failed.") % (output[:proxy_task_id]) else on_data(response['actions'].find { |block_action| block_action['class'] == proxy_action_name }['output']) end else suspend end else process_timeout end end def cancel_proxy_task if output[:cancel_sent] error! ForemanTasks::Task::TaskCancelledException.new(_("Cancel enforced: the task might be still running on the proxy")) else proxy.cancel_task(output[:proxy_task_id]) output[:cancel_sent] = true suspend end end def on_resume # TODO: add logic to load the data from the external action suspend end # @override to put custom logic on event handling def on_data(data) output[:proxy_output] = data end # @override String name of an action to be triggered on server def proxy_action_name raise NotImplemented end def proxy ProxyAPI::ForemanDynflow::DynflowProxy.new(:url => input[:proxy_url]) end def proxy_output output[:proxy_output] end def proxy_output=(output) output[:proxy_output] = output end def metadata output[:metadata] ||= {} output[:metadata] end def metadata=(thing) output[:metadata] ||= {} output[:metadata] = thing end def timeout_set? !metadata[:timeout].nil? end def set_timeout! time = Time.now + input[:connection_options][:timeout] schedule_timeout(time) metadata[:timeout] = time.to_s end def default_connection_options # Fails if the plan is not finished within 60 seconds from the first task trigger attempt on the smart proxy # If the triggering fails, it retries 3 more times with 15 second delays { :retry_interval => Setting['foreman_tasks_proxy_action_retry_interval'] || 15, :retry_count => Setting['foreman_tasks_proxy_action_retry_count' ] || 4, :timeout => Setting['foreman_tasks_proxy_action_start_timeout'] || 60 } end private def with_connection_error_handling(event = nil) yield event rescue ::RestClient::Exception, Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Errno::ETIMEDOUT => e if event.class == CallbackData || event == ::Dynflow::Action::Timeouts::Timeout raise e else handle_connection_exception(e, event) end end def format_exception(exception) { :proxy_task_id => output[:proxy_task_id], :exception_class => exception.class.name, :exception_message => exception.message, :timestamp => Time.now.to_f } end def handle_connection_exception(exception, event = nil) metadata[:failed_proxy_tasks] ||= [] options = input[:connection_options] metadata[:failed_proxy_tasks] << format_exception(exception) output[:proxy_task_id] = nil if metadata[:failed_proxy_tasks].count < options[:retry_count] suspend do |suspended_action| @world.clock.ping suspended_action, Time.now + options[:retry_interval], event end else raise exception end end end end