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 +4 -4
- data/lib/app/jobs/cron/trim_command_jobs.rb +28 -0
- data/lib/app/models/command_job.rb +374 -0
- data/lib/app/models/command_job_log.rb +33 -0
- data/lib/web47core.rb +3 -0
- data/lib/web47core/version.rb +1 -1
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: dbf707fe805b12f85070027a7f991404b327d1abf62b633b2b0489740e902e5b
|
4
|
+
data.tar.gz: c0fef2df3709e4319297a1f55c0c0b0968c83a89451f264511e2663f236925f0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
data/lib/web47core/version.rb
CHANGED
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.
|
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-
|
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
|