web47core 2.0.1 → 2.2.15
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +111 -41
- data/app/controllers/status_controller.rb +8 -3
- 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/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/trim_collection.rb +1 -1
- 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 +12 -11
- data/lib/app/models/concerns/api_tokenable.rb +38 -0
- data/lib/app/models/concerns/aws_configuration.rb +65 -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 +18 -201
- data/lib/app/models/concerns/delayed_job_configuration.rb +24 -0
- data/lib/app/models/concerns/email_able.rb +6 -2
- data/lib/app/models/concerns/google_sso_configuration.rb +32 -0
- 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 +5 -2
- 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/sms_notification.rb +9 -6
- data/lib/app/models/template.rb +12 -12
- data/lib/web47core/version.rb +1 -1
- data/lib/web47core.rb +33 -9
- metadata +114 -69
@@ -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
|
@@ -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
|
@@ -110,7 +110,7 @@ class CommandJob
|
|
110
110
|
end
|
111
111
|
|
112
112
|
def failure_or_cancelled?
|
113
|
-
job_state?([STATE_FAIL, STATE_CANCELLED], true)
|
113
|
+
job_state?([STATE_FAIL, STATE_CANCELLED], default_state: true)
|
114
114
|
end
|
115
115
|
|
116
116
|
#
|
@@ -118,11 +118,11 @@ class CommandJob
|
|
118
118
|
# state. If there is a match, then return true, otherwise return false.
|
119
119
|
# If there is an error, return the default.
|
120
120
|
#
|
121
|
-
def job_state?(states,
|
121
|
+
def job_state?(states, default_state: false)
|
122
122
|
states.is_a?(Array) ? states.include?(state) : states.eql?(state)
|
123
123
|
rescue StandardError => error
|
124
|
-
App47Logger.log_warn "Unable to check job failed or cancelled #{
|
125
|
-
|
124
|
+
App47Logger.log_warn "Unable to check job failed or cancelled #{inspect}", error
|
125
|
+
default_state
|
126
126
|
end
|
127
127
|
|
128
128
|
#
|
@@ -323,8 +323,9 @@ class CommandJob
|
|
323
323
|
else
|
324
324
|
logs.create!(dir: dir, command: command, message: output)
|
325
325
|
end
|
326
|
-
|
327
|
-
check_for_text(output, options[:
|
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])
|
328
329
|
output
|
329
330
|
end
|
330
331
|
|
@@ -343,16 +344,17 @@ class CommandJob
|
|
343
344
|
|
344
345
|
#
|
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
|
346
348
|
#
|
347
|
-
def check_for_text(output, texts = [],
|
349
|
+
def check_for_text(output, texts = [], inclusive_check: true, output_limit: -1)
|
348
350
|
return if texts.blank?
|
349
351
|
|
350
352
|
texts = [texts] if texts.is_a?(String)
|
351
353
|
texts.each do |text|
|
352
|
-
if
|
353
|
-
raise "Error: found text (#{text}) - #{output}" if output.match?(/#{text}/)
|
354
|
+
if inclusive_check
|
355
|
+
raise "Error: found text (#{text}) - #{output[0...output_limit]}" if output.match?(/#{text}/)
|
354
356
|
else
|
355
|
-
raise "Error: missing text (#{text}) - #{output}" unless output.match?(/#{text}/)
|
357
|
+
raise "Error: missing text (#{text}) - #{output[0...output_limit]}" unless output.match?(/#{text}/)
|
356
358
|
end
|
357
359
|
end
|
358
360
|
end
|
@@ -370,5 +372,4 @@ class CommandJob
|
|
370
372
|
def sort_fields
|
371
373
|
%i[created_at]
|
372
374
|
end
|
373
|
-
|
374
375
|
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
# API tokenable, support for the api token in user, but also may be applied elsewhere
|
5
|
+
#
|
6
|
+
module ApiTokenable
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
def self.included(base)
|
10
|
+
base.class_eval do
|
11
|
+
# store api token
|
12
|
+
field :api_token, type: String
|
13
|
+
# if the api token should be reset
|
14
|
+
field :reset_api_token, type: Boolean, default: true
|
15
|
+
field :last_authenticated_at, type: Time
|
16
|
+
field :last_authenticated_ip, type: String
|
17
|
+
# call back to reset the api token.
|
18
|
+
before_save :assign_api_token, if: :reset_api_token
|
19
|
+
# set the index on api token
|
20
|
+
index({ api_token: 1 }, background: true)
|
21
|
+
|
22
|
+
def cycle_api_token
|
23
|
+
self.set api_token: SecureRandom.urlsafe_base64
|
24
|
+
api_token
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
#
|
30
|
+
# set the api token
|
31
|
+
#
|
32
|
+
def assign_api_token
|
33
|
+
self.reset_api_token = false
|
34
|
+
self.api_token = SecureRandom.urlsafe_base64
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
# AWS Configuration
|
5
|
+
#
|
6
|
+
module AwsConfiguration
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
def self.included(base)
|
10
|
+
base.class_eval do
|
11
|
+
#
|
12
|
+
# Fields
|
13
|
+
#
|
14
|
+
field :aws_region, type: String
|
15
|
+
field :aws_access_key_id, type: String
|
16
|
+
field :aws_secret_access_key, type: String
|
17
|
+
field :aws_auto_scaling_group_name, type: String
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
#
|
22
|
+
# Make sure the password doesn't get blanked out on an update
|
23
|
+
#
|
24
|
+
def secure_fields
|
25
|
+
super + %i[aws_secret_access_key]
|
26
|
+
end
|
27
|
+
|
28
|
+
#
|
29
|
+
# Determine if AWS is configured
|
30
|
+
#
|
31
|
+
def aws_configured?
|
32
|
+
[aws_region.present?, aws_access_key_id.present?, aws_secret_access_key.present?].all?
|
33
|
+
end
|
34
|
+
|
35
|
+
#
|
36
|
+
# Determine if auto scaling group is configured
|
37
|
+
#
|
38
|
+
def aws_auto_scaling_configured?
|
39
|
+
aws_configured? && aws_auto_scaling_group_name.present?
|
40
|
+
end
|
41
|
+
|
42
|
+
#
|
43
|
+
# AWS client.
|
44
|
+
#
|
45
|
+
def aws_ec2_client
|
46
|
+
return nil unless aws_configured?
|
47
|
+
|
48
|
+
@aws_ec2_client ||= Aws::EC2::Client.new(region: aws_region,
|
49
|
+
credentials: Aws::Credentials.new(aws_access_key_id,
|
50
|
+
aws_secret_access_key))
|
51
|
+
end
|
52
|
+
|
53
|
+
#
|
54
|
+
# S3 Client
|
55
|
+
#
|
56
|
+
def aws_s3_client
|
57
|
+
return nil unless aws_configured?
|
58
|
+
|
59
|
+
# We want this to remake itself each time because it is possible that the
|
60
|
+
# => user would change the access keys in between actions. Huh?
|
61
|
+
@aws_s3_client ||= Aws::S3::Client.new(region: aws_vault_bucket_region,
|
62
|
+
access_key_id: aws_access_key_id,
|
63
|
+
secret_access_key: aws_secret_access_key)
|
64
|
+
end
|
65
|
+
end
|
@@ -17,6 +17,13 @@
|
|
17
17
|
#
|
18
18
|
module CdnUrl
|
19
19
|
extend ActiveSupport::Concern
|
20
|
+
#
|
21
|
+
# Constants
|
22
|
+
#
|
23
|
+
STYLE_S3_FILE_PATH = ':class/:attachment/:id/:style.:extension' unless defined? STYLE_S3_FILE_PATH
|
24
|
+
STYLE_FILE_PATH = 'public/system/:class/:attachment/:id/:style.:extension' unless defined? STYLE_FILE_PATH
|
25
|
+
STYLE_S3_FILE_URL = ':s3_domain_url' unless defined? STYLE_S3_FILE_URL
|
26
|
+
STYLE_FILE_URL = ':rails_root/public/system/:class/:attachment/:id/:style.:extension' unless defined? STYLE_FILE_URL
|
20
27
|
|
21
28
|
def method_missing(method, *args)
|
22
29
|
if method.to_s.start_with? 'cdn_'
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
# SMTP Configuration
|
5
|
+
#
|
6
|
+
module CoreSmtpConfiguration
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
def self.included(base)
|
10
|
+
base.class_eval do
|
11
|
+
#
|
12
|
+
# Fields
|
13
|
+
#
|
14
|
+
field :default_email, type: String, default: 'support@app47.com'
|
15
|
+
field :support_email, type: String, default: 'support@app47.com'
|
16
|
+
field :smtp_name, type: String
|
17
|
+
field :smtp_address, type: String
|
18
|
+
field :smtp_domain, type: String
|
19
|
+
field :smtp_port, type: Integer, default: 587
|
20
|
+
field :smtp_user_name, type: String
|
21
|
+
field :smtp_password, type: String
|
22
|
+
field :smtp_enable_starttls_auto, type: Boolean, default: false
|
23
|
+
field :mailgun_api_key, type: String
|
24
|
+
field :email_notification_ttl, type: Integer, default: 180
|
25
|
+
end
|
26
|
+
base.extend SmtpClassMethods
|
27
|
+
end
|
28
|
+
|
29
|
+
#
|
30
|
+
# Class methods for smtp configuration
|
31
|
+
#
|
32
|
+
module SmtpClassMethods
|
33
|
+
def smtp_configuration
|
34
|
+
output = {}
|
35
|
+
config = configuration
|
36
|
+
fields = %w[name address domain port user_name password enable_starttls_auto]
|
37
|
+
fields.each do |field|
|
38
|
+
field_name = "smtp_#{field}".to_sym
|
39
|
+
output[field.to_sym] = config.send(field_name)
|
40
|
+
end
|
41
|
+
output
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
#
|
46
|
+
# Make sure the password doesn't get blanked out on an update
|
47
|
+
#
|
48
|
+
def secure_fields
|
49
|
+
super + %i[smtp_password mailgun_api_key]
|
50
|
+
end
|
51
|
+
|
52
|
+
#
|
53
|
+
# Determine if SMTP is configured
|
54
|
+
#
|
55
|
+
def smtp_configured?
|
56
|
+
smtp_name.present? && smtp_address.present? && smtp_domain.present?
|
57
|
+
end
|
58
|
+
|
59
|
+
#
|
60
|
+
# Determine if mailgun is configured
|
61
|
+
#
|
62
|
+
def mail_gun_configured?
|
63
|
+
smtp_configured? && mailgun_api_key.present?
|
64
|
+
end
|
65
|
+
end
|