Class: Actions::ForemanOpenbolt::PollTaskStatus

Inherits:
EntryAction
  • Object
show all
Defined in:
app/lib/actions/foreman_openbolt/poll_task_status.rb

Constant Summary collapse

POLL_INTERVAL =
5.seconds
RETRY_LIMIT =

5 minutes at 5-second intervals

60

Instance Method Summary collapse

Instance Method Details

#exception(msg, e) ⇒ Object



36
37
38
39
# File 'app/lib/actions/foreman_openbolt/poll_task_status.rb', line 36

def exception(msg, e)
  log("#{msg}: #{e.class}: #{e.message}", :error)
  log(e.backtrace.join("\n"), :error) if e.backtrace
end

#extract_proxy_error(response) ⇒ Object



45
46
47
48
49
50
# File 'app/lib/actions/foreman_openbolt/poll_task_status.rb', line 45

def extract_proxy_error(response)
  error_value = response&.dig('error')
  return nil if error_value.nil? || (error_value.respond_to?(:empty?) && error_value.empty?)

  error_value.is_a?(Hash) ? error_value['message'] || error_value.to_s : error_value.to_s
end

#finishObject



41
42
43
# File 'app/lib/actions/foreman_openbolt/poll_task_status.rb', line 41

def finish
  log("Polling finished for OpenBolt job #{input[:job_id]}")
end

#humanized_inputObject



152
153
154
155
156
157
158
# File 'app/lib/actions/foreman_openbolt/poll_task_status.rb', line 152

def humanized_input
  proxy_name = ::SmartProxy.find_by(id: input[:proxy_id])&.name || '(unknown)'
  task_name = ::ForemanOpenbolt::TaskJob.find_by(job_id: input[:job_id])&.task_name
  parts = ["job #{input[:job_id]} on #{proxy_name}"]
  parts << "task: #{task_name}" if task_name
  parts.join(', ')
end

#humanized_nameObject



148
149
150
# File 'app/lib/actions/foreman_openbolt/poll_task_status.rb', line 148

def humanized_name
  _('Poll OpenBolt task execution status')
end

#log(msg, level = :debug) ⇒ Object



30
31
32
33
34
# File 'app/lib/actions/foreman_openbolt/poll_task_status.rb', line 30

def log(msg, level = :debug)
  output[:log] ||= []
  output[:log] << "[#{Time.now.getlocal.strftime('%Y-%m-%d %H:%M:%S')}] [#{level.upcase}] #{msg}"
  Rails.logger.send(level, msg)
end

#plan(job_id, proxy_id) ⇒ Object

Set up the action when it is first scheduled, storing IDs needed to get information from the proxy.



14
15
16
# File 'app/lib/actions/foreman_openbolt/poll_task_status.rb', line 14

def plan(job_id, proxy_id)
  plan_self(job_id: job_id, proxy_id: proxy_id)
end

#poll_and_rescheduleObject



52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
# File 'app/lib/actions/foreman_openbolt/poll_task_status.rb', line 52

def poll_and_reschedule
  job_id = input[:job_id]
  task_job = ::ForemanOpenbolt::TaskJob.find_by(job_id: job_id)

  if task_job.nil?
    log("TaskJob record not found for job #{job_id}", :error)
    finish
    return
  end

  if task_job.completed?
    finish
    return
  end

  # If the smart proxy has been deleted somehow or is unknown,
  # we can't poll for status, so finish.
  proxy = ::SmartProxy.find_by(id: input[:proxy_id])
  unless proxy
    log("Smart Proxy with ID #{input[:proxy_id]} not found for OpenBolt job #{job_id}", :error)
    task_job.update!(status: 'exception')
    finish
    return
  end

  begin
    api = ::ProxyAPI::Openbolt.new(url: proxy.url)

    # Fetch current status
    status_result = api.job_status(job_id: job_id)

    proxy_error = extract_proxy_error(status_result)
    if proxy_error
      log("Proxy returned error for job #{job_id}: #{proxy_error}", :error)
      task_job.update!(status: 'exception')
      finish
      return
    end

    unless status_result&.dig('status')
      log("Proxy returned response without status for job #{job_id}: #{status_result.inspect}", :error)
      task_job.update!(status: 'exception')
      finish
      return
    end

    input[:retry_count] = 0
    new_status = status_result['status']
    if new_status == task_job.status
      log("Poll for OpenBolt job #{job_id}: status=#{new_status}")
    else
      previous_status = task_job.status
      task_job.update!(status: new_status)
      log("OpenBolt job #{job_id} status changed from '#{previous_status}' to '#{new_status}'", :info)
    end

    # If completed, fetch full results
    if task_job.completed?
      result = api.job_result(job_id: job_id)
      if result.present?
        task_job.update_from_proxy_result!(result)
        log("OpenBolt job #{job_id} completed with status '#{task_job.status}'", :info)
      else
        log("No result returned from proxy for completed OpenBolt job #{job_id}", :error)
      end
      finish
      return
    end

    # Still running, schedule next poll in 5 seconds
    suspend do |suspended_action|
      world.clock.ping(suspended_action, POLL_INTERVAL.from_now.to_time, :poll)
    end
  rescue StandardError => e
    exception("Error polling task status for job #{job_id}", e)

    retry_count = (input[:retry_count] || 0) + 1
    input[:retry_count] = retry_count

    if retry_count > RETRY_LIMIT
      log("Polling gave up for job #{job_id} after #{retry_count} attempts", :error)
      task_job.update!(status: 'exception')
      finish
      return
    end

    suspend do |suspended_action|
      world.clock.ping(suspended_action, POLL_INTERVAL.from_now.to_time, :poll)
    end
  end
end

#rescue_strategyObject



144
145
146
# File 'app/lib/actions/foreman_openbolt/poll_task_status.rb', line 144

def rescue_strategy
  Dynflow::Action::Rescue::Skip
end

#run(event = nil) ⇒ Object

Main execution method that Dynflow will call repeatedly. event = nil when execution starts event = :poll when this is triggered by the timer



21
22
23
24
25
26
27
28
# File 'app/lib/actions/foreman_openbolt/poll_task_status.rb', line 21

def run(event = nil)
  if event.nil? || event.to_sym == :poll
    poll_and_reschedule
  else
    log("Received unknown event '#{event}' for OpenBolt job #{input[:job_id]}", :error)
    finish
  end
end