Class: CommandJob

Inherits:
Object
  • Object
show all
Includes:
StandardModel
Defined in:
lib/app/models/command_job.rb

Overview

Base class for all jobs that will be run on builds

Constant Summary collapse

STATE_NEW =

Constants

'new'
STATE_WIP =
'working'
STATE_RETRYING =
'retrying'
STATE_SUCCESS =
'success'
STATE_FAIL =
'failure'
STATE_CANCELLED =
'cancelled'
ALL_STATES =
[STATE_NEW, STATE_WIP, STATE_RETRYING, STATE_CANCELLED, STATE_SUCCESS, STATE_FAIL].freeze

Instance Method Summary collapse

Methods included from StandardModel

#audit_action, #auto_strip_attributes, #capture_user_info, #clear_cache, #created_by_display_name, #delete_and_log, #destroy_and_log, included, #last_modified_by_display_name, #log_change, #log_deletion, #remove_blank_secure_fields, #save_and_log, #save_and_log!, #secure_fields, #update, #update!, #update_and_log, #update_and_log!

Methods included from App47Logger

clean_params, #clean_params, delete_parameter_keys, #log_controller_error, log_debug, #log_debug, log_error, #log_error, log_exception, #log_message, log_message, #log_warn, log_warn, mask_parameter_keys, #update_flash_messages

Instance Method Details

#add_log(message) ⇒ Object

Add a job log message



383
384
385
# File 'lib/app/models/command_job.rb', line 383

def add_log(message)
  logs.create!(message: message)
end

#after_runObject

Steps to execute after a run



198
199
200
201
202
203
204
205
206
207
# File 'lib/app/models/command_job.rb', line 198

def after_run
  case state
  when STATE_RETRYING, STATE_WIP
    set finished_at: Time.now.utc, error_message: nil, state: STATE_SUCCESS
  when STATE_SUCCESS
    set finished_at: Time.now.utc, error_message: nil
  else
    set finished_at: Time.now.utc
  end
end

#before_runObject

Steps to execute before a run



168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
# File 'lib/app/models/command_job.rb', line 168

def before_run
  case state
  when STATE_NEW
    set retries: 0,
        started_at: Time.now.utc,
        finished_at: nil,
        error_message: nil,
        result: nil,
        state: STATE_WIP
  when STATE_RETRYING
    set retries: 0,
        started_at: Time.now.utc,
        finished_at: nil,
        error_message: nil,
        result: nil
  when STATE_FAIL
    set retries: 0,
        started_at: Time.now.utc,
        finished_at: nil,
        error_message: nil,
        state: STATE_RETRYING,
        result: nil
  else
    set retries: 0, started_at: Time.now.utc, finished_at: nil, result: nil
  end
end

#cancel!(actor) ⇒ Object

This method is abstract.

cancel the current job



43
44
45
46
# File 'lib/app/models/command_job.rb', line 43

def cancel!(actor)
  update!(cancelled_by: actor, cancelled_at: Time.now.utc, state: STATE_CANCELLED) unless completed?
  child_jobs.each { |j| j.cancel!(actor) }
end

#cancelled?Boolean

If we are cancelled

Returns:

  • (Boolean)


126
127
128
# File 'lib/app/models/command_job.rb', line 126

def cancelled?
  job_state?(STATE_CANCELLED)
end

#check_for_text(output, texts = [], inclusive_check: true, output_limit: -1)) ⇒ Object

Check if any occurrences were found (or not found) For most command jobs, we want to see the full output. -1 accomplishes this



367
368
369
370
371
372
373
374
375
376
377
378
# File 'lib/app/models/command_job.rb', line 367

def check_for_text(output, texts = [], inclusive_check: true, output_limit: -1)
  return if texts.blank?

  texts = [texts] if texts.is_a?(String)
  texts.each do |text|
    if inclusive_check
      raise "Error: found text (#{text}) - #{output[0...output_limit]}" if output.match?(/#{text}/)
    else
      raise "Error: missing text (#{text}) - #{output[0...output_limit]}" unless output.match?(/#{text}/)
    end
  end
end

#child_jobsObject



48
49
50
# File 'lib/app/models/command_job.rb', line 48

def child_jobs
  []
end

#completed?Boolean

If we is finished, failed or success

Returns:

  • (Boolean)


110
111
112
# File 'lib/app/models/command_job.rb', line 110

def completed?
  job_state?([STATE_CANCELLED, STATE_FAIL, STATE_SUCCESS])
end

#copy_dir(dir, to_path) ⇒ Object

Copy a given directory to a new location and record the log



284
285
286
287
# File 'lib/app/models/command_job.rb', line 284

def copy_dir(dir, to_path)
  FileUtils.cp_r dir, to_path
  add_log "Copy directory from: #{dir} to: #{to_path}"
end

#copy_file(from_path, to_path) ⇒ Object

Copy a given file to a new location and record the log



270
271
272
273
274
275
276
277
278
279
# File 'lib/app/models/command_job.rb', line 270

def copy_file(from_path, to_path)
  if File.exist? from_path
    FileUtils.cp(from_path, to_path)
    add_log "Copy file from: #{from_path} to: #{to_path}"
  else
    add_log "File not found: #{from_path}, copy not performed"
  end
rescue StandardError => error
  raise "Unable to copy file from #{from_path} to #{to_path}, error: ##{error.message}"
end

#current_statusObject

Return the job’s status and information in a hash that could be used to return to a calling api



150
151
152
153
154
# File 'lib/app/models/command_job.rb', line 150

def current_status
  status = { state: state }
  status[:message] = error_message if error_message.present?
  status
end

#display_started_byObject

This method is abstract.

Who started this job

Returns String - Who started this job if present, otherwise ‘System`.

Returns:

  • String - Who started this job if present, otherwise ‘System`



62
63
64
# File 'lib/app/models/command_job.rb', line 62

def display_started_by
  started_by.present? ? started_by.name : 'System'
end

#download_file(file_url, file_path) ⇒ Object

Download a file to the given path



259
260
261
262
263
264
265
# File 'lib/app/models/command_job.rb', line 259

def download_file(file_url, file_path)
  download = URI.parse(file_url).open
  IO.copy_stream(download, file_path)
  add_log "Downloaded file: #{file_url} to #{file_path}"
rescue StandardError => error
  raise "Unable to download file from #{file_url} to #{file_path}, error: ##{error.message}"
end

#durationObject

This method is abstract.

duration for the job

Returns Integer - How long this job took in milliseconds.

Returns:

  • Integer - How long this job took in milliseconds



54
55
56
57
58
# File 'lib/app/models/command_job.rb', line 54

def duration
  succeeded? ? finished_at - started_at : 0
rescue StandardError
  0
end

#failure?Boolean

True if in fail status

Returns:

  • (Boolean)


103
104
105
# File 'lib/app/models/command_job.rb', line 103

def failure?
  job_state?(STATE_FAIL)
end

#failure_or_cancelled?Boolean

Returns:

  • (Boolean)


130
131
132
# File 'lib/app/models/command_job.rb', line 130

def failure_or_cancelled?
  job_state?([STATE_FAIL, STATE_CANCELLED], default_state: true)
end

#job_state?(states, default_state: false) ⇒ Boolean

Fetch the latest version of this instance from the database and check the state against the required state. If there is a match, then return true, otherwise return false. If there is an error, return the default.

Returns:

  • (Boolean)


139
140
141
142
143
144
# File 'lib/app/models/command_job.rb', line 139

def job_state?(states, default_state: false)
  states.is_a?(Array) ? states.include?(state) : states.eql?(state)
rescue StandardError => error
  App47Logger.log_warn "Unable to check job failed or cancelled #{inspect}", error
  default_state
end

#mask_keywords(output, keywords = []) ⇒ Object

Mask keywords if given in the command



353
354
355
356
357
358
359
360
361
# File 'lib/app/models/command_job.rb', line 353

def mask_keywords(output, keywords = [])
  return output if keywords.blank?

  keywords = [keywords] if keywords.is_a?(String)
  keywords.each do |keyword|
    output = output.gsub(keyword, '***********')
  end
  output
end

#mkdir(dir) ⇒ Object Also known as: make_dir

Create a directory and record it



312
313
314
315
316
317
# File 'lib/app/models/command_job.rb', line 312

def mkdir(dir)
  return if File.exist?(dir)

  FileUtils.mkdir dir
  add_log "Created directory: #{dir}"
end

#nameObject

Return the name of this job



75
76
77
# File 'lib/app/models/command_job.rb', line 75

def name
  self.class.to_s.underscore.humanize
end

#new_job?Boolean

True if in new status

Returns:

  • (Boolean)


82
83
84
# File 'lib/app/models/command_job.rb', line 82

def new_job?
  job_state?(STATE_NEW)
end

#performObject Also known as: perform_now

Perform the command job



213
214
215
216
217
218
219
220
# File 'lib/app/models/command_job.rb', line 213

def perform
  before_run
  run
  after_run
rescue StandardError => error
  log_error 'Unable to start job', error
  set state: STATE_FAIL, error_message: error.message
end

#perform_laterObject

Perform this job in the background



159
160
161
# File 'lib/app/models/command_job.rb', line 159

def perform_later
  perform
end

#remove_dir(dir_path) ⇒ Object

Remove the given file name



302
303
304
305
306
307
# File 'lib/app/models/command_job.rb', line 302

def remove_dir(dir_path)
  return unless File.exist?(dir_path)

  FileUtils.remove_dir dir_path
  add_log "Removing dir: #{dir_path}"
end

#remove_file(file_path) ⇒ Object

Remove the given file name



292
293
294
295
296
297
# File 'lib/app/models/command_job.rb', line 292

def remove_file(file_path)
  return unless File.exist?(file_path)

  FileUtils.remove_file file_path
  add_log "Removing file: #{file_path}"
end

#runObject

Run the job, handling any failures that might happen



227
228
229
230
231
232
233
234
235
236
237
238
239
# File 'lib/app/models/command_job.rb', line 227

def run
  run! unless cancelled?
rescue StandardError => error
  if (retries + 1) >= max_retries
    log_error "Unable to run job id: #{id}, done retrying", error
    set state: STATE_FAIL, error_message: "Failed final attempt: #{error.message}"
  else
    log_error "Unable to run job id: #{id}, retrying!!", error
    add_log "Unable to run job: #{error.message}, retrying!!"
    set error_message: "Failed attempt # #{retries}: #{error.message}", retries: retries + 1, state: STATE_RETRYING
    run
  end
end

#run!Object

Determine the correct action to take and get it started



244
245
246
# File 'lib/app/models/command_job.rb', line 244

def run!
  raise 'Incomplete class, concrete implementation should implement #run!'
end

#run_command(command, dir = '/tmp', options = {}) ⇒ Object

Run the command capturing the command output and any standard error to the log.



331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
# File 'lib/app/models/command_job.rb', line 331

def run_command(command, dir = '/tmp', options = {})
  command = command.join(' ') if command.is_a?(Array)
  output = Tempfile.open('run-command-', '/tmp') do |f|
    Dir.chdir(dir) { `#{command} > #{f.path} 2>&1` }
    mask_keywords(f.open.read, options[:mask_texts])
  end
  output = 'Success' if output.blank?
  command = mask_keywords(command, options[:mask_texts])
  if block_given?
    yield output
  else
    logs.create!(dir: dir, command: command, message: output)
  end
  options[:output_limit] ||= -1
  check_for_text(output, options[:error_texts], output_limit: options[:output_limit])
  check_for_text(output, options[:required_texts], inclusive_check: false, output_limit: options[:output_limit])
  output
end

#running?Boolean Also known as: incomplete?

Job has not finished, failure or success

Returns:

  • (Boolean)


117
118
119
# File 'lib/app/models/command_job.rb', line 117

def running?
  !completed?
end

#sort_fieldsObject

Which to sort by



390
391
392
# File 'lib/app/models/command_job.rb', line 390

def sort_fields
  %i[created_at]
end

#succeeded?Boolean

True if in success status

Returns:

  • (Boolean)


96
97
98
# File 'lib/app/models/command_job.rb', line 96

def succeeded?
  job_state?(STATE_SUCCESS)
end

#ttlObject

This method is abstract.

Default time to keep a job before auto archiving it

Returns Integer - TTL for this job.

Returns:

  • Integer - TTL for this job



68
69
70
# File 'lib/app/models/command_job.rb', line 68

def ttl
  30
end

#unzip_file(file_path, to_dir) ⇒ Object

Unzip a given file



324
325
326
# File 'lib/app/models/command_job.rb', line 324

def unzip_file(file_path, to_dir)
  run_command "unzip #{file_path}", to_dir, error_texts: 'unzip:'
end

#work_in_progress?Boolean

True if in WIP status

Returns:

  • (Boolean)


89
90
91
# File 'lib/app/models/command_job.rb', line 89

def work_in_progress?
  job_state?([STATE_WIP, STATE_RETRYING])
end

#write_file(path, contents) ⇒ Object

Write out the contents to the file



251
252
253
254
# File 'lib/app/models/command_job.rb', line 251

def write_file(path, contents)
  File.open(path, 'w') { |f| f.write(contents) }
  add_log "Saving:\n #{contents}\nto: #{path}"
end