web47core 2.0.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +111 -41
  3. data/app/controllers/status_controller.rb +15 -4
  4. data/app/helpers/core_form_helper.rb +6 -6
  5. data/app/helpers/core_helper.rb +1 -1
  6. data/app/helpers/core_link_helper.rb +47 -8
  7. data/app/helpers/core_nav_bar_helper.rb +5 -4
  8. data/app/helpers/core_table_helper.rb +80 -0
  9. data/app/helpers/model_modal_helper.rb +3 -3
  10. data/app/views/common/_create_actions.html.haml +12 -0
  11. data/app/views/common/_update_actions.html.haml +10 -0
  12. data/app/views/cron/_edit.html.haml +15 -17
  13. data/app/views/cron/_index.html.haml +74 -67
  14. data/app/views/delayed_job_metrics/_index.html.haml +27 -0
  15. data/app/views/delayed_job_metrics/index.html.haml +1 -0
  16. data/app/views/delayed_job_workers/_index.html.haml +27 -0
  17. data/app/views/delayed_job_workers/index.html.haml +1 -0
  18. data/app/views/delayed_jobs/_index.html.haml +47 -52
  19. data/app/views/delayed_jobs/_show.html.haml +15 -13
  20. data/app/views/system_configurations/_edit.html.haml +14 -9
  21. data/app/views/system_configurations/_show.html.haml +18 -12
  22. data/config/brakeman.ignore +26 -0
  23. data/config/brakeman.yml +2 -0
  24. data/config/locales/en.yml +21 -3
  25. data/lib/app/controllers/concerns/core_delayed_job_metrics_controller.rb +35 -0
  26. data/lib/app/controllers/concerns/core_delayed_job_workers_controller.rb +35 -0
  27. data/lib/app/controllers/concerns/core_delayed_jobs_controller.rb +2 -3
  28. data/lib/app/jobs/application_job.rb +0 -1
  29. data/lib/app/jobs/cron/command.rb +1 -4
  30. data/lib/app/jobs/cron/record_delayed_job_metrics.rb +25 -0
  31. data/lib/app/jobs/cron/restart_orphaned_delayed_jobs.rb +44 -0
  32. data/lib/app/jobs/cron/server.rb +32 -15
  33. data/lib/app/jobs/cron/switchboard_sync_configuration.rb +2 -0
  34. data/lib/app/jobs/cron/switchboard_sync_models.rb +2 -0
  35. data/lib/app/jobs/cron/tab.rb +1 -1
  36. data/lib/app/jobs/cron/trim_collection.rb +1 -1
  37. data/lib/app/jobs/cron/trim_command_jobs.rb +28 -0
  38. data/lib/app/jobs/cron/trim_delayed_job_metrics.rb +29 -0
  39. data/lib/app/jobs/cron/trim_delayed_job_workers.rb +39 -0
  40. data/lib/app/jobs/cron/trim_failed_delayed_jobs.rb +2 -0
  41. data/lib/app/models/api_token.rb +9 -0
  42. data/lib/app/models/command_job.rb +375 -0
  43. data/lib/app/models/command_job_log.rb +33 -0
  44. data/lib/app/models/concerns/api_tokenable.rb +38 -0
  45. data/lib/app/models/concerns/aws_configuration.rb +41 -0
  46. data/lib/app/models/concerns/cdn_url.rb +7 -0
  47. data/lib/app/models/concerns/core_smtp_configuration.rb +65 -0
  48. data/lib/app/models/concerns/core_system_configuration.rb +21 -204
  49. data/lib/app/models/concerns/delayed_job_configuration.rb +24 -0
  50. data/lib/app/models/concerns/email_able.rb +7 -3
  51. data/lib/app/models/concerns/search_able.rb +16 -0
  52. data/lib/app/models/concerns/server_process_able.rb +69 -0
  53. data/lib/app/models/concerns/slack_configuration.rb +38 -0
  54. data/lib/app/models/concerns/standard_model.rb +8 -9
  55. data/lib/app/models/concerns/switchboard_configuration.rb +43 -0
  56. data/lib/app/models/concerns/twilio_configuration.rb +37 -0
  57. data/lib/app/models/concerns/zendesk_configuration.rb +92 -0
  58. data/lib/app/models/{delayed_job.rb → delayed/backend/delayed_job.rb} +41 -0
  59. data/lib/app/models/delayed/jobs/metric.rb +61 -0
  60. data/lib/app/models/delayed/jobs/run.rb +40 -0
  61. data/lib/app/models/delayed/jobs/worker.rb +43 -0
  62. data/lib/app/models/delayed/plugins/time_keeper.rb +33 -0
  63. data/lib/app/models/delayed/worker.rb +24 -0
  64. data/lib/app/models/email_notification.rb +2 -1
  65. data/lib/app/models/email_template.rb +5 -6
  66. data/lib/app/models/notification.rb +12 -2
  67. data/lib/app/models/notification_template.rb +1 -1
  68. data/lib/app/models/sms_notification.rb +9 -6
  69. data/lib/app/models/smtp_configuration.rb +3 -3
  70. data/lib/app/models/template.rb +12 -12
  71. data/lib/web47core/version.rb +1 -1
  72. data/lib/web47core.rb +35 -9
  73. metadata +147 -216
@@ -0,0 +1,44 @@
1
+ #
2
+ # Run in cron
3
+ #
4
+ module Cron
5
+ #
6
+ # Cycle through all members and tell them to sync with with switchboard
7
+ #
8
+ class RestartOrphanedDelayedJobs < Job
9
+ cron_tab_entry :hourly
10
+
11
+ #
12
+ # Only run when we have background jobs and we have the time keeper plugin installed
13
+ #
14
+ def self.valid_environment?
15
+ Delayed::Worker.plugins.include?(Delayed::Plugins::TimeKeeper) &&
16
+ SystemConfiguration.delayed_job_restart_orphaned? &&
17
+ Delayed::Backend::Mongoid::Job.count.positive?
18
+ rescue StandardError
19
+ false
20
+ end
21
+
22
+ #
23
+ # Cycle through delayed jobs, looking for running jobs
24
+ # skip if we don't have any records or low sample set
25
+ # skip if the allowed time is less than allowed by the job
26
+ # look to see if have a worker associated with the delayed job
27
+ # If we dont have a worker or the worker is dead, restart the job
28
+ #
29
+ def execute
30
+ Delayed::Backend::Mongoid::Job.each do |delayed_job|
31
+ next unless delayed_job.running?
32
+
33
+ metric = Delayed::Jobs::Metric.where(name: delayed_job.display_name).first
34
+ next if metric.blank? || metric.count < 30 # not enough data to make a call
35
+
36
+ run_time = Time.now.utc - delayed_job.locked_at
37
+ next if run_time < metric.max_allowed_seconds # still within parameters
38
+
39
+ worker = delayed_job.worker
40
+ delayed_job.resubmit if worker.blank? || worker.dead?
41
+ end
42
+ end
43
+ end
44
+ end
@@ -6,6 +6,7 @@ module Cron
6
6
  #
7
7
  class Server
8
8
  include StandardModel
9
+ include ServerProcessAble
9
10
  #
10
11
  # Constants
11
12
  #
@@ -15,20 +16,19 @@ module Cron
15
16
  #
16
17
  # Fields
17
18
  #
18
- field :host_name, type: String
19
- field :pid, type: Integer
20
19
  field :desired_server_count, type: Integer, default: 0
21
20
  field :current_server_count, type: Integer, default: 0
22
- field :last_check_in_at, type: Time, default: Time.now.utc
23
21
  field :state, type: String, default: STATE_SECONDARY
24
22
  #
25
23
  # Validations
26
24
  #
27
- validates :host_name, presence: true
28
- validates :pid, presence: true
29
- validates :last_check_in_at, presence: true
30
25
  validates :state, inclusion: { in: ALL_STATES }
31
26
  validate :high_lander
27
+ #
28
+ # Callback
29
+ #
30
+ before_validation :ensure_last_check_in
31
+
32
32
  #
33
33
  # Go through the logic once a minute
34
34
  #
@@ -89,7 +89,7 @@ module Cron
89
89
  # Warm up a server on the next evaluation
90
90
  #
91
91
  def self.warm_up_server
92
- return unless SystemConfiguration.aws_auto_scaling_configured?
92
+ return unless auto_scaling_configured?
93
93
 
94
94
  primary_server.auto_scale([primary_server.desired_server_count + 1, 10].min)
95
95
  end
@@ -148,18 +148,11 @@ module Cron
148
148
  !alive?
149
149
  end
150
150
 
151
- #
152
- # Perform a check in for the server
153
- #
154
- def check_in
155
- set({ last_check_in_at: Time.now.utc })
156
- end
157
-
158
151
  #
159
152
  # Auto scale environment
160
153
  #
161
154
  def check_auto_scale
162
- return unless SystemConfiguration.aws_auto_scaling_configured?
155
+ return unless auto_scaling_configured?
163
156
 
164
157
  if delayed_jobs_count.eql?(0)
165
158
  handle_zero_job_count
@@ -181,6 +174,24 @@ module Cron
181
174
  @sys_config ||= SystemConfiguration.configuration
182
175
  end
183
176
 
177
+ #
178
+ # Test if autoscaling is configured, return false if there is an error
179
+ #
180
+ def auto_scaling_configured?
181
+ @auto_scaling_configured ||= sys_config.aws_auto_scaling_configured?
182
+ rescue StandardError
183
+ false
184
+ end
185
+
186
+ #
187
+ # Test if autoscaling is configured, return false if there is an error
188
+ #
189
+ def self.auto_scaling_configured?
190
+ SystemConfiguration.aws_auto_scaling_configured?
191
+ rescue StandardError
192
+ false
193
+ end
194
+
184
195
  #
185
196
  # Returns the AutoScalingGroup associated with the account
186
197
  #
@@ -258,6 +269,8 @@ module Cron
258
269
  client.update_auto_scaling_group(auto_scaling_group_name: sys_config.aws_auto_scaling_group_name,
259
270
  min_size: desired_count,
260
271
  desired_capacity: desired_count)
272
+ rescue StandardError => error
273
+ App47Logger.log_error "Unable to set auto scaler to #{desired_count}", error
261
274
  end
262
275
 
263
276
  #
@@ -283,5 +296,9 @@ module Cron
283
296
  def inactive_count
284
297
  desired_server_count
285
298
  end
299
+
300
+ def ensure_last_check_in
301
+ self.last_check_in_at ||= Time.now.utc
302
+ end
286
303
  end
287
304
  end
@@ -13,6 +13,8 @@ module Cron
13
13
  #
14
14
  def self.valid_environment?
15
15
  SystemConfiguration.switchboard_configured?
16
+ rescue StandardError
17
+ false
16
18
  end
17
19
 
18
20
  #
@@ -13,6 +13,8 @@ module Cron
13
13
  #
14
14
  def self.valid_environment?
15
15
  SystemConfiguration.switchboard_configured? && Web47core::Config.switchboard_able_models.present?
16
+ rescue StandardError
17
+ false
16
18
  end
17
19
 
18
20
  #
@@ -18,7 +18,7 @@ module Cron
18
18
  # Fields
19
19
  #
20
20
  field :name, type: String
21
- field :enabled, type: Boolean, default: true
21
+ field :enabled, type: Mongoid::Boolean, default: true
22
22
  field :min, type: String, default: 0
23
23
  field :hour, type: String, default: 0
24
24
  field :wday, type: String, default: WILDCARD
@@ -57,7 +57,7 @@ module Cron
57
57
  # Allowed time the amount of time allowed to exists before deleting
58
58
  #
59
59
  def allowed_time
60
- 30.days.ago
60
+ 30.days.ago.utc
61
61
  end
62
62
  end
63
63
  end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cron
4
+ #
5
+ # Clean up Jobs
6
+ #
7
+ class TrimCommandJobs < TrimCollection
8
+ #
9
+ # Fetch each Audit Log and delete it if hasn't been updated in 90 days
10
+ #
11
+ def collection
12
+ CommandJob.all
13
+ end
14
+
15
+ #
16
+ # Check which audit logs we wanted deleted
17
+ #
18
+ # Should be older than 90 days and either not a user model audit log or the model associated with
19
+ # the UserModelAuditLog has been deleted
20
+ #
21
+ def allowed_time_for_item(job)
22
+ job.ttl.days.ago.utc
23
+ rescue StandardError => error
24
+ App47Logger.log_warn "Unable to determine if job should be archived: #{job.inspect}", error
25
+ 30.days.ago.utc
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cron
4
+ #
5
+ # Clean up Audit Logs that have not been updated in 90 days
6
+ #
7
+ class TrimDelayedJobMetrics < TrimCollection
8
+ #
9
+ # Fetch each Audit Log and delete it if hasn't been updated in 90 days
10
+ #
11
+ def collection
12
+ Delayed::Jobs::Metric.all
13
+ end
14
+
15
+ #
16
+ # Return which field to use for comparison when trimming objects
17
+ #
18
+ def comparison_field
19
+ :last_run_at
20
+ end
21
+
22
+ #
23
+ # Allowed time the amount of time allowed to exists before deleting
24
+ #
25
+ def allowed_time
26
+ 12.months.ago.utc
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cron
4
+ #
5
+ # Clean up Audit Logs that have not been updated in 90 days
6
+ #
7
+ class TrimDelayedJobWorkers < TrimCollection
8
+ #
9
+ # Fetch each Audit Log and delete it if hasn't been updated in 90 days
10
+ #
11
+ def collection
12
+ Delayed::Jobs::Worker.all
13
+ end
14
+
15
+ #
16
+ # Check if the cron job server hasn't reported in a while
17
+ #
18
+ def archive?(worker)
19
+ super && worker.runs.blank?
20
+ rescue StandardError => error
21
+ App47Logger.log_warn "Unable to archive item #{worker.inspect}", error
22
+ false
23
+ end
24
+
25
+ #
26
+ # Return which field to use for comparison when trimming objects
27
+ #
28
+ def comparison_field
29
+ :last_check_in_at
30
+ end
31
+
32
+ #
33
+ # Allowed time the amount of time allowed to exists before deleting
34
+ #
35
+ def allowed_time
36
+ 2.days.ago.utc
37
+ end
38
+ end
39
+ end
@@ -35,6 +35,8 @@ module Cron
35
35
  #
36
36
  def self.valid_environment?
37
37
  SystemConfiguration.slack_api_url.present?
38
+ rescue StandardError
39
+ false
38
40
  end
39
41
  end
40
42
  end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # A specific API token to be used as part of a collection of tokens
5
+ #
6
+ class ApiToken
7
+ include StandardModel
8
+ include ApiTokenable
9
+ end
@@ -0,0 +1,375 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Base class for all jobs that will be run on builds
5
+ #
6
+ class CommandJob
7
+ include StandardModel
8
+ #
9
+ # Constants
10
+ #
11
+ STATE_NEW = 'new' unless defined? STATE_NEW
12
+ STATE_WIP = 'working' unless defined? STATE_WIP
13
+ STATE_RETRYING = 'retrying' unless defined? STATE_RETRYING
14
+ STATE_SUCCESS = 'success' unless defined? STATE_SUCCESS
15
+ STATE_FAIL = 'failure' unless defined? STATE_FAIL
16
+ STATE_CANCELLED = 'cancelled' unless defined? STATE_CANCELLED
17
+ unless defined? ALL_STATES
18
+ ALL_STATES = [STATE_NEW, STATE_WIP, STATE_RETRYING, STATE_CANCELLED, STATE_SUCCESS, STATE_FAIL].freeze
19
+ end
20
+ #
21
+ # Fields
22
+ #
23
+ field :state, type: String, default: STATE_NEW
24
+ field :retries, type: Integer, default: 0
25
+ field :max_retries, type: Integer, default: 5
26
+ field :result, type: String
27
+ field :error_message, type: String
28
+ field :started_at, type: Time
29
+ field :finished_at, type: Time
30
+ #
31
+ # Relationships
32
+ #
33
+ has_many :logs, class_name: 'CommandJobLog', dependent: :destroy
34
+ belongs_to :started_by, class_name: 'User', optional: true
35
+ #
36
+ # Validations
37
+ #
38
+ validates :state, inclusion: { in: ALL_STATES }
39
+
40
+ #
41
+ # Who started this job
42
+ #
43
+ def display_started_by
44
+ started_by.present? ? started_by.name : 'System'
45
+ end
46
+
47
+ #
48
+ # Default time to keep a job before auto archiving it
49
+ #
50
+ def ttl
51
+ 30
52
+ end
53
+
54
+ #
55
+ # Return the name of this job
56
+ #
57
+ def name
58
+ self.class.to_s.underscore.humanize
59
+ end
60
+
61
+ #
62
+ # True if in new status
63
+ #
64
+ def new_job?
65
+ job_state?(STATE_NEW)
66
+ end
67
+
68
+ #
69
+ # True if in WIP status
70
+ #
71
+ def work_in_progress?
72
+ job_state?([STATE_WIP, STATE_RETRYING])
73
+ end
74
+
75
+ #
76
+ # True if in success status
77
+ #
78
+ def succeeded?
79
+ job_state?(STATE_SUCCESS)
80
+ end
81
+
82
+ #
83
+ # True if in fail status
84
+ #
85
+ def failure?
86
+ job_state?(STATE_FAIL)
87
+ end
88
+
89
+ #
90
+ # If we is finished, failed or success
91
+ #
92
+ def completed?
93
+ job_state?([STATE_CANCELLED, STATE_FAIL, STATE_SUCCESS])
94
+ end
95
+
96
+ #
97
+ # Job has not finished, failure or success
98
+ #
99
+ def running?
100
+ !completed?
101
+ end
102
+
103
+ alias incomplete? running?
104
+
105
+ #
106
+ # If we are cancelled
107
+ #
108
+ def cancelled?
109
+ job_state?(STATE_CANCELLED)
110
+ end
111
+
112
+ def failure_or_cancelled?
113
+ job_state?([STATE_FAIL, STATE_CANCELLED], default_state: true)
114
+ end
115
+
116
+ #
117
+ # Fetch the latest version of this instance from the database and check the state against the required
118
+ # state. If there is a match, then return true, otherwise return false.
119
+ # If there is an error, return the default.
120
+ #
121
+ def job_state?(states, default_state: false)
122
+ states.is_a?(Array) ? states.include?(state) : states.eql?(state)
123
+ rescue StandardError => error
124
+ App47Logger.log_warn "Unable to check job failed or cancelled #{inspect}", error
125
+ default_state
126
+ end
127
+
128
+ #
129
+ # Return the job's status and information in a hash that could be used to return to a calling
130
+ # api
131
+ #
132
+ def current_status
133
+ status = { state: state }
134
+ status[:message] = error_message if error_message.present?
135
+ status
136
+ end
137
+
138
+ #
139
+ # Perform this job in the background
140
+ #
141
+ def perform_later
142
+ perform
143
+ end
144
+
145
+ handle_asynchronously :perform_later
146
+
147
+ #
148
+ # Steps to execute before a run
149
+ #
150
+ def before_run
151
+ case state
152
+ when STATE_NEW
153
+ set retries: 0,
154
+ started_at: Time.now.utc,
155
+ finished_at: nil,
156
+ error_message: nil,
157
+ result: nil,
158
+ state: STATE_WIP
159
+ when STATE_RETRYING
160
+ set retries: 0,
161
+ started_at: Time.now.utc,
162
+ finished_at: nil,
163
+ error_message: nil,
164
+ result: nil
165
+ when STATE_FAIL
166
+ set retries: 0,
167
+ started_at: Time.now.utc,
168
+ finished_at: nil,
169
+ error_message: nil,
170
+ state: STATE_RETRYING,
171
+ result: nil
172
+ else
173
+ set retries: 0, started_at: Time.now.utc, finished_at: nil, result: nil
174
+ end
175
+ end
176
+
177
+ #
178
+ # Steps to execute after a run
179
+ #
180
+ def after_run
181
+ case state
182
+ when STATE_RETRYING, STATE_WIP
183
+ set finished_at: Time.now.utc, error_message: nil, state: STATE_SUCCESS
184
+ when STATE_SUCCESS
185
+ set finished_at: Time.now.utc, error_message: nil
186
+ else
187
+ set finished_at: Time.now.utc
188
+ end
189
+ end
190
+
191
+ #
192
+ #
193
+ # Perform the command job
194
+ #
195
+ def perform
196
+ before_run
197
+ run
198
+ after_run
199
+ rescue StandardError => error
200
+ log_error 'Unable to start job', error
201
+ set state: STATE_FAIL, error_message: error.message
202
+ end
203
+
204
+ alias perform_now perform
205
+
206
+ #
207
+ # Run the job, handling any failures that might happen
208
+ #
209
+ def run
210
+ run! unless cancelled?
211
+ rescue StandardError => error
212
+ if (retries + 1) >= max_retries
213
+ log_error "Unable to run job id: #{id}, done retrying", error
214
+ set state: STATE_FAIL, error_message: "Failed final attempt: #{error.message}"
215
+ else
216
+ log_error "Unable to run job id: #{id}, retrying!!", error
217
+ add_log "Unable to run job: #{error.message}, retrying!!"
218
+ set error_message: "Failed attempt # #{retries}: #{error.message}", retries: retries + 1, state: STATE_RETRYING
219
+ run
220
+ end
221
+ end
222
+
223
+ #
224
+ # Determine the correct action to take and get it started
225
+ #
226
+ def run!
227
+ raise 'Incomplete class, concrete implementation should implement #run!'
228
+ end
229
+
230
+ #
231
+ # Write out the contents to the file
232
+ #
233
+ def write_file(path, contents)
234
+ File.open(path, 'w') { |f| f.write(contents) }
235
+ add_log "Saving:\n #{contents}\nto: #{path}"
236
+ end
237
+
238
+ #
239
+ # Download a file to the given path
240
+ #
241
+ def download_file(file_url, file_path)
242
+ download = URI.parse(file_url).open
243
+ IO.copy_stream(download, file_path)
244
+ add_log "Downloaded file: #{file_url} to #{file_path}"
245
+ rescue StandardError => error
246
+ raise "Unable to download file from #{file_url} to #{file_path}, error: ##{error.message}"
247
+ end
248
+
249
+ #
250
+ # Copy a given file to a new location and record the log
251
+ #
252
+ def copy_file(from_path, to_path)
253
+ if File.exist? from_path
254
+ FileUtils.cp(from_path, to_path)
255
+ add_log "Copy file from: #{from_path} to: #{to_path}"
256
+ else
257
+ add_log "File not found: #{from_path}, copy not performed"
258
+ end
259
+ rescue StandardError => error
260
+ raise "Unable to copy file from #{from_path} to #{to_path}, error: ##{error.message}"
261
+ end
262
+
263
+ #
264
+ # Copy a given directory to a new location and record the log
265
+ #
266
+ def copy_dir(dir, to_path)
267
+ FileUtils.cp_r dir, to_path
268
+ add_log "Copy directory from: #{dir} to: #{to_path}"
269
+ end
270
+
271
+ #
272
+ # Remove the given file name
273
+ #
274
+ def remove_file(file_path)
275
+ return unless File.exist?(file_path)
276
+
277
+ FileUtils.remove_file file_path
278
+ add_log "Removing file: #{file_path}"
279
+ end
280
+
281
+ #
282
+ # Remove the given file name
283
+ #
284
+ def remove_dir(dir_path)
285
+ return unless File.exist?(dir_path)
286
+
287
+ FileUtils.remove_dir dir_path
288
+ add_log "Removing dir: #{dir_path}"
289
+ end
290
+
291
+ #
292
+ # Create a directory and record it
293
+ #
294
+ def mkdir(dir)
295
+ return if File.exist?(dir)
296
+
297
+ FileUtils.mkdir dir
298
+ add_log "Created directory: #{dir}"
299
+ end
300
+
301
+ alias make_dir mkdir
302
+
303
+ #
304
+ # Unzip a given file
305
+ #
306
+ def unzip_file(file_path, to_dir)
307
+ run_command "unzip #{file_path}", to_dir, error_texts: 'unzip:'
308
+ end
309
+
310
+ #
311
+ # Run the command capturing the command output and any standard error to the log.
312
+ #
313
+ def run_command(command, dir = '/tmp', options = {})
314
+ command = command.join(' ') if command.is_a?(Array)
315
+ output = Tempfile.open('run-command-', '/tmp') do |f|
316
+ Dir.chdir(dir) { `#{command} > #{f.path} 2>&1` }
317
+ mask_keywords(f.open.read, options[:mask_texts])
318
+ end
319
+ output = 'Success' if output.blank?
320
+ command = mask_keywords(command, options[:mask_texts])
321
+ if block_given?
322
+ yield output
323
+ else
324
+ logs.create!(dir: dir, command: command, message: output)
325
+ end
326
+ options[:output_limit] ||= -1
327
+ check_for_text(output, options[:error_texts], output_limit: options[:output_limit])
328
+ check_for_text(output, options[:required_texts], inclusive_check: false, output_limit: options[:output_limit])
329
+ output
330
+ end
331
+
332
+ #
333
+ # Mask keywords if given in the command
334
+ #
335
+ def mask_keywords(output, keywords = [])
336
+ return output if keywords.blank?
337
+
338
+ keywords = [keywords] if keywords.is_a?(String)
339
+ keywords.each do |keyword|
340
+ output = output.gsub(keyword, '***********')
341
+ end
342
+ output
343
+ end
344
+
345
+ #
346
+ # Check if any occurrences were found (or not found)
347
+ # For most command jobs, we want to see the full output. -1 accomplishes this
348
+ #
349
+ def check_for_text(output, texts = [], inclusive_check: true, output_limit: -1)
350
+ return if texts.blank?
351
+
352
+ texts = [texts] if texts.is_a?(String)
353
+ texts.each do |text|
354
+ if inclusive_check
355
+ raise "Error: found text (#{text}) - #{output[0...output_limit]}" if output.match?(/#{text}/)
356
+ else
357
+ raise "Error: missing text (#{text}) - #{output[0...output_limit]}" unless output.match?(/#{text}/)
358
+ end
359
+ end
360
+ end
361
+
362
+ #
363
+ # Add a job log message
364
+ #
365
+ def add_log(message)
366
+ logs.create!(message: message)
367
+ end
368
+
369
+ #
370
+ # Which to sort by
371
+ #
372
+ def sort_fields
373
+ %i[created_at]
374
+ end
375
+ end