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.
- checksums.yaml +4 -4
- data/README.md +111 -41
- data/app/controllers/status_controller.rb +15 -4
- data/app/helpers/core_form_helper.rb +6 -6
- data/app/helpers/core_helper.rb +1 -1
- data/app/helpers/core_link_helper.rb +47 -8
- data/app/helpers/core_nav_bar_helper.rb +5 -4
- data/app/helpers/core_table_helper.rb +80 -0
- data/app/helpers/model_modal_helper.rb +3 -3
- data/app/views/common/_create_actions.html.haml +12 -0
- data/app/views/common/_update_actions.html.haml +10 -0
- data/app/views/cron/_edit.html.haml +15 -17
- data/app/views/cron/_index.html.haml +74 -67
- data/app/views/delayed_job_metrics/_index.html.haml +27 -0
- data/app/views/delayed_job_metrics/index.html.haml +1 -0
- data/app/views/delayed_job_workers/_index.html.haml +27 -0
- data/app/views/delayed_job_workers/index.html.haml +1 -0
- data/app/views/delayed_jobs/_index.html.haml +47 -52
- data/app/views/delayed_jobs/_show.html.haml +15 -13
- data/app/views/system_configurations/_edit.html.haml +14 -9
- data/app/views/system_configurations/_show.html.haml +18 -12
- data/config/brakeman.ignore +26 -0
- data/config/brakeman.yml +2 -0
- data/config/locales/en.yml +21 -3
- data/lib/app/controllers/concerns/core_delayed_job_metrics_controller.rb +35 -0
- data/lib/app/controllers/concerns/core_delayed_job_workers_controller.rb +35 -0
- data/lib/app/controllers/concerns/core_delayed_jobs_controller.rb +2 -3
- data/lib/app/jobs/application_job.rb +0 -1
- data/lib/app/jobs/cron/command.rb +1 -4
- data/lib/app/jobs/cron/record_delayed_job_metrics.rb +25 -0
- data/lib/app/jobs/cron/restart_orphaned_delayed_jobs.rb +44 -0
- data/lib/app/jobs/cron/server.rb +32 -15
- data/lib/app/jobs/cron/switchboard_sync_configuration.rb +2 -0
- data/lib/app/jobs/cron/switchboard_sync_models.rb +2 -0
- data/lib/app/jobs/cron/tab.rb +1 -1
- data/lib/app/jobs/cron/trim_collection.rb +1 -1
- data/lib/app/jobs/cron/trim_command_jobs.rb +28 -0
- data/lib/app/jobs/cron/trim_delayed_job_metrics.rb +29 -0
- data/lib/app/jobs/cron/trim_delayed_job_workers.rb +39 -0
- data/lib/app/jobs/cron/trim_failed_delayed_jobs.rb +2 -0
- data/lib/app/models/api_token.rb +9 -0
- data/lib/app/models/command_job.rb +375 -0
- data/lib/app/models/command_job_log.rb +33 -0
- data/lib/app/models/concerns/api_tokenable.rb +38 -0
- data/lib/app/models/concerns/aws_configuration.rb +41 -0
- data/lib/app/models/concerns/cdn_url.rb +7 -0
- data/lib/app/models/concerns/core_smtp_configuration.rb +65 -0
- data/lib/app/models/concerns/core_system_configuration.rb +21 -204
- data/lib/app/models/concerns/delayed_job_configuration.rb +24 -0
- data/lib/app/models/concerns/email_able.rb +7 -3
- data/lib/app/models/concerns/search_able.rb +16 -0
- data/lib/app/models/concerns/server_process_able.rb +69 -0
- data/lib/app/models/concerns/slack_configuration.rb +38 -0
- data/lib/app/models/concerns/standard_model.rb +8 -9
- data/lib/app/models/concerns/switchboard_configuration.rb +43 -0
- data/lib/app/models/concerns/twilio_configuration.rb +37 -0
- data/lib/app/models/concerns/zendesk_configuration.rb +92 -0
- data/lib/app/models/{delayed_job.rb → delayed/backend/delayed_job.rb} +41 -0
- data/lib/app/models/delayed/jobs/metric.rb +61 -0
- data/lib/app/models/delayed/jobs/run.rb +40 -0
- data/lib/app/models/delayed/jobs/worker.rb +43 -0
- data/lib/app/models/delayed/plugins/time_keeper.rb +33 -0
- data/lib/app/models/delayed/worker.rb +24 -0
- data/lib/app/models/email_notification.rb +2 -1
- data/lib/app/models/email_template.rb +5 -6
- data/lib/app/models/notification.rb +12 -2
- data/lib/app/models/notification_template.rb +1 -1
- data/lib/app/models/sms_notification.rb +9 -6
- data/lib/app/models/smtp_configuration.rb +3 -3
- data/lib/app/models/template.rb +12 -12
- data/lib/web47core/version.rb +1 -1
- data/lib/web47core.rb +35 -9
- 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
|
data/lib/app/jobs/cron/server.rb
CHANGED
@@ -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
|
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
|
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
|
data/lib/app/jobs/cron/tab.rb
CHANGED
@@ -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
|
@@ -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
|
@@ -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
|