web47core 2.0.0 → 2.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2eee94f84ba4767b90179b2572ed33e3ba7f60112524fb7b7cd2af8613c42524
4
- data.tar.gz: 3ae3b9cedbadabbb98db834d7a4c3fe8e071dd3ccf9f2b18d19f8ec5086fd055
3
+ metadata.gz: dbf707fe805b12f85070027a7f991404b327d1abf62b633b2b0489740e902e5b
4
+ data.tar.gz: c0fef2df3709e4319297a1f55c0c0b0968c83a89451f264511e2663f236925f0
5
5
  SHA512:
6
- metadata.gz: 4cf27f3ef39691a4c1f13c37381bb574bff398895740018fae76215e22532ed8d9f78dd69f05286fa140a15831f580d1c353924f00c01bc5feb4aeb3e14af64e
7
- data.tar.gz: a336720edc072530a94a748715f5c37cf179743354636af54a773599892e6a3dc804fe9bc0ceb0274078dc1f9d2a4b7281e9379aa9176a8617c65dedb6629cba
6
+ metadata.gz: d15d90c313d88d4ad813b98a764508484554b47b790d55b688f1bb156a8b1a0b350ba709fee7be2f26f6db3e83954c461b3f274fedcddb7756c6de9336df7f44
7
+ data.tar.gz: feeac4239229604c458fb02a292e820f42ff789c361afa9b1a1b958e1057e3b1b99cff995b0bd12b2c6e5621df092f88eed179b3027d02c83d883fd79ed19249
@@ -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,374 @@
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], 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 = 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 #{self.inspect}", error
125
+ default
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
+ check_for_text(output, options[:error_texts], true)
327
+ check_for_text(output, options[:required_texts], false)
328
+ output
329
+ end
330
+
331
+ #
332
+ # Mask keywords if given in the command
333
+ #
334
+ def mask_keywords(output, keywords = [])
335
+ return output if keywords.blank?
336
+
337
+ keywords = [keywords] if keywords.is_a?(String)
338
+ keywords.each do |keyword|
339
+ output = output.gsub(keyword, '***********')
340
+ end
341
+ output
342
+ end
343
+
344
+ #
345
+ # Check if any occurrences were found (or not found)
346
+ #
347
+ def check_for_text(output, texts = [], inclusive = true)
348
+ return if texts.blank?
349
+
350
+ texts = [texts] if texts.is_a?(String)
351
+ texts.each do |text|
352
+ if inclusive
353
+ raise "Error: found text (#{text}) - #{output}" if output.match?(/#{text}/)
354
+ else
355
+ raise "Error: missing text (#{text}) - #{output}" unless output.match?(/#{text}/)
356
+ end
357
+ end
358
+ end
359
+
360
+ #
361
+ # Add a job log message
362
+ #
363
+ def add_log(message)
364
+ logs.create!(message: message)
365
+ end
366
+
367
+ #
368
+ # Which to sort by
369
+ #
370
+ def sort_fields
371
+ %i[created_at]
372
+ end
373
+
374
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Capture command log messages for a given job
5
+ #
6
+ class CommandJobLog
7
+ include StandardModel
8
+ #
9
+ # Fields
10
+ #
11
+ field :message, type: String
12
+ field :command, type: String
13
+ field :dir, type: String
14
+ #
15
+ # Relationships
16
+ #
17
+ belongs_to :job, inverse_of: :logs, class_name: 'CommandJob'
18
+ #
19
+ # Validations
20
+ #
21
+ validates :message, presence: true
22
+
23
+ #
24
+ # Display message
25
+ #
26
+ def display_message
27
+ if dir.present?
28
+ "Dir: #{dir}\nCommand: #{command}\nOutput: #{message}"
29
+ else
30
+ message
31
+ end
32
+ end
33
+ end
data/lib/web47core.rb CHANGED
@@ -12,6 +12,8 @@ require 'app/models/concerns/core_system_configuration'
12
12
  require 'app/models/concerns/core_account'
13
13
  require 'app/models/concerns/secure_fields'
14
14
  require 'app/models/concerns/encrypted_password'
15
+ require 'app/models/command_job'
16
+ require 'app/models/command_job_log'
15
17
  require 'app/models/delayed_job'
16
18
  require 'app/models/redis_configuration'
17
19
  require 'app/models/notification'
@@ -38,6 +40,7 @@ require 'app/jobs/cron/switchboard_sync_models'
38
40
  require 'app/jobs/cron/trim_audit_logs'
39
41
  require 'app/jobs/cron/trim_cron_servers'
40
42
  require 'app/jobs/cron/trim_failed_delayed_jobs'
43
+ require 'app/jobs/cron/trim_command_jobs'
41
44
  require 'app/jobs/cron/trim_notifications'
42
45
  #
43
46
  # Audit Logs
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Web47core
4
- VERSION = '2.0.0'
4
+ VERSION = '2.0.1'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: web47core
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 2.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chris Schroeder
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-03-03 00:00:00.000000000 Z
11
+ date: 2021-03-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -714,10 +714,13 @@ files:
714
714
  - lib/app/jobs/cron/tab.rb
715
715
  - lib/app/jobs/cron/trim_audit_logs.rb
716
716
  - lib/app/jobs/cron/trim_collection.rb
717
+ - lib/app/jobs/cron/trim_command_jobs.rb
717
718
  - lib/app/jobs/cron/trim_cron_servers.rb
718
719
  - lib/app/jobs/cron/trim_failed_delayed_jobs.rb
719
720
  - lib/app/jobs/cron/trim_notifications.rb
720
721
  - lib/app/models/audit_log.rb
722
+ - lib/app/models/command_job.rb
723
+ - lib/app/models/command_job_log.rb
721
724
  - lib/app/models/concerns/app47_logger.rb
722
725
  - lib/app/models/concerns/cdn_url.rb
723
726
  - lib/app/models/concerns/cipher_able.rb