web47core 2.0.0 → 2.0.1

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.
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