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



365
366
367
# File 'lib/app/models/command_job.rb', line 365

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

#after_runObject

Steps to execute after a run



180
181
182
183
184
185
186
187
188
189
# File 'lib/app/models/command_job.rb', line 180

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



150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
# File 'lib/app/models/command_job.rb', line 150

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

#cancelled?Boolean

If we are cancelled

Returns:

  • (Boolean)


108
109
110
# File 'lib/app/models/command_job.rb', line 108

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



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

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

#completed?Boolean

If we is finished, failed or success

Returns:

  • (Boolean)


92
93
94
# File 'lib/app/models/command_job.rb', line 92

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



266
267
268
269
# File 'lib/app/models/command_job.rb', line 266

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



252
253
254
255
256
257
258
259
260
261
# File 'lib/app/models/command_job.rb', line 252

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



132
133
134
135
136
# File 'lib/app/models/command_job.rb', line 132

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

#display_started_byObject

Who started this job



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

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



241
242
243
244
245
246
247
# File 'lib/app/models/command_job.rb', line 241

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

#failure?Boolean

True if in fail status

Returns:

  • (Boolean)


85
86
87
# File 'lib/app/models/command_job.rb', line 85

def failure?
  job_state?(STATE_FAIL)
end

#failure_or_cancelled?Boolean

Returns:

  • (Boolean)


112
113
114
# File 'lib/app/models/command_job.rb', line 112

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)


121
122
123
124
125
126
# File 'lib/app/models/command_job.rb', line 121

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



335
336
337
338
339
340
341
342
343
# File 'lib/app/models/command_job.rb', line 335

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



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

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

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

#nameObject

Return the name of this job



57
58
59
# File 'lib/app/models/command_job.rb', line 57

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

#new_job?Boolean

True if in new status

Returns:

  • (Boolean)


64
65
66
# File 'lib/app/models/command_job.rb', line 64

def new_job?
  job_state?(STATE_NEW)
end

#performObject Also known as: perform_now

Perform the command job



195
196
197
198
199
200
201
202
# File 'lib/app/models/command_job.rb', line 195

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



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

def perform_later
  perform
end

#remove_dir(dir_path) ⇒ Object

Remove the given file name



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

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



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

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



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

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



226
227
228
# File 'lib/app/models/command_job.rb', line 226

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.



313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
# File 'lib/app/models/command_job.rb', line 313

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)


99
100
101
# File 'lib/app/models/command_job.rb', line 99

def running?
  !completed?
end

#sort_fieldsObject

Which to sort by



372
373
374
# File 'lib/app/models/command_job.rb', line 372

def sort_fields
  %i[created_at]
end

#succeeded?Boolean

True if in success status

Returns:

  • (Boolean)


78
79
80
# File 'lib/app/models/command_job.rb', line 78

def succeeded?
  job_state?(STATE_SUCCESS)
end

#ttlObject

Default time to keep a job before auto archiving it



50
51
52
# File 'lib/app/models/command_job.rb', line 50

def ttl
  30
end

#unzip_file(file_path, to_dir) ⇒ Object

Unzip a given file



306
307
308
# File 'lib/app/models/command_job.rb', line 306

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)


71
72
73
# File 'lib/app/models/command_job.rb', line 71

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

#write_file(path, contents) ⇒ Object

Write out the contents to the file



233
234
235
236
# File 'lib/app/models/command_job.rb', line 233

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