marty 1.1.5 → 1.1.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/app/controllers/marty/diagnostic_controller.rb +15 -416
- data/app/models/diagnostic/aws/ec2_instance.rb +100 -0
- data/app/models/diagnostic/base.rb +69 -0
- data/app/models/diagnostic/base_collection.rb +10 -0
- data/app/models/diagnostic/collection.rb +10 -0
- data/app/models/diagnostic/delayed_job.rb +48 -0
- data/app/models/diagnostic/env.rb +35 -0
- data/app/models/diagnostic/environment.rb +35 -0
- data/app/models/diagnostic/fatal.rb +12 -0
- data/app/models/diagnostic/helper.rb +11 -0
- data/app/models/diagnostic/nodes.rb +18 -0
- data/app/models/diagnostic/reporter.rb +108 -0
- data/app/models/diagnostic/request.rb +28 -0
- data/app/models/diagnostic/version.rb +17 -0
- data/app/models/marty/helper.rb +0 -8
- data/app/views/marty/diagnostic/diag.html.erb +15 -19
- data/app/views/marty/diagnostic/op.html.erb +18 -19
- data/delorean/diagnostics.dl +1 -1
- data/lib/diagnostic/database.rb +28 -0
- data/lib/diagnostic/node.rb +35 -0
- data/lib/diagnostic/packer.rb +47 -0
- data/lib/marty/version.rb +1 -1
- data/spec/controllers/diagnostic_controller_spec.rb +18 -175
- data/spec/models/diagnostic/base_spec.rb +98 -0
- data/spec/models/diagnostic/collection_spec.rb +32 -0
- data/spec/models/diagnostic/delayed_job_spec.rb +46 -0
- data/spec/models/diagnostic/reporter_spec.rb +319 -0
- metadata +21 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bea1f7001c31f2ac05b9b5694618355e46661702
|
4
|
+
data.tar.gz: 6345a3b84a2319b1a25c2408dd134e371ff0eed1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f4ac10c766f9e4532065c95b75abc71b0d9d79fea51a83a3c3a0c5ba1bed72a287441d84e2c30efa1186ac8d1cc5ed109e9489d3aa557529e2091be723182486
|
7
|
+
data.tar.gz: dc8397f83c0acfbe180e3d7a7228b473d4e13bef61c5faf094abdb45d5dcac7cbfa419aa8d93944de57807f365130c5c28b676494f95813c3b31b19ffcdd3e10
|
data/Gemfile.lock
CHANGED
@@ -1,433 +1,32 @@
|
|
1
|
-
include ActionView::Helpers::TextHelper
|
2
|
-
require 'erb'
|
3
|
-
|
4
1
|
module Marty
|
5
2
|
class DiagnosticController < ActionController::Base
|
6
|
-
layout false
|
7
3
|
def op
|
8
4
|
begin
|
9
|
-
|
10
|
-
Base.request = request
|
11
|
-
|
12
|
-
# determine if request is aggregate and return result
|
13
|
-
params[:scope] = 'nodal' unless params[:scope]
|
14
|
-
diag = self.class.get_sub_class(params[:op])
|
15
|
-
@result = params[:scope] == 'local' ? diag.generate : diag.aggregate
|
5
|
+
@result = Diagnostic::Reporter.run(request)
|
16
6
|
rescue NameError
|
17
7
|
render file: 'public/400', formats: [:html], status: 400, layout: false
|
18
8
|
else
|
19
9
|
respond_to do |format|
|
20
|
-
format.html {@result =
|
21
|
-
format.json {render json:
|
10
|
+
format.html {@result = display_parameters}
|
11
|
+
format.json {render json: process_result_for_api}
|
22
12
|
end
|
23
13
|
end
|
24
14
|
end
|
25
15
|
|
26
|
-
def
|
27
|
-
|
16
|
+
def process_result_for_api
|
17
|
+
@result.delete('data') unless request.params['data'] == 'true'
|
18
|
+
@result.delete('errors') if @result['errors'] && @result['errors'].empty?
|
19
|
+
@result
|
28
20
|
end
|
29
21
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
@@read_only = Marty::Util.db_in_recovery?
|
39
|
-
@@template = ActionController::Base.new.lookup_context.
|
40
|
-
find_template("marty/diagnostic/diag").identifier
|
41
|
-
|
42
|
-
def self.request
|
43
|
-
@@request
|
44
|
-
end
|
45
|
-
|
46
|
-
def self.request= req
|
47
|
-
@@request = req
|
48
|
-
end
|
49
|
-
|
50
|
-
def self.aggregate op_name=name.demodulize
|
51
|
-
get_nodal_diags(op_name)
|
52
|
-
end
|
53
|
-
|
54
|
-
def self.get_nodal_diags op_name, scope='local'
|
55
|
-
self.get_nodes.map do |n|
|
56
|
-
ssl = ENV['HTTPS'] == 'on'
|
57
|
-
uri = Addressable::URI.new(host: n, port: ssl ? 443 : @@request.port)
|
58
|
-
uri.query_values = {op: op_name.underscore,
|
59
|
-
scope: scope}
|
60
|
-
uri.scheme = ssl ? 'https' : 'http'
|
61
|
-
uri.path = '/marty/diag.json'
|
62
|
-
opts = {ssl_verify_mode: OpenSSL::SSL::VERIFY_NONE}
|
63
|
-
{n => JSON.parse(open(uri, opts).readlines[0])}
|
64
|
-
end.sum
|
65
|
-
end
|
66
|
-
|
67
|
-
def self.find_failures data
|
68
|
-
data.each_with_object({}){
|
69
|
-
|(k,v), h| h[k] = v.include?('Failure') ? 'F' : 'P'}
|
70
|
-
end
|
71
|
-
|
72
|
-
def self.errors data
|
73
|
-
data.keys.count{|n| is_failure?(data[n])}
|
74
|
-
end
|
75
|
-
|
76
|
-
def self.diff data
|
77
|
-
data.keys.map{|n| data[n]}.uniq.length != 1
|
78
|
-
end
|
79
|
-
|
80
|
-
def self.package message
|
81
|
-
{name.demodulize => message}
|
82
|
-
end
|
83
|
-
|
84
|
-
def self.is_failure? message
|
85
|
-
message.to_s.include?('Failure')
|
86
|
-
end
|
87
|
-
|
88
|
-
def self.error message
|
89
|
-
"Failure: #{message}"
|
90
|
-
end
|
91
|
-
|
92
|
-
# determine "target" (highest) value for tests
|
93
|
-
def self.get_targets data
|
94
|
-
data.each_with_object({}) do |(_, v), h|
|
95
|
-
v.each do |k, r|
|
96
|
-
r = r.to_s
|
97
|
-
h[k] ||= r
|
98
|
-
h[k] = r if h[k] < r
|
99
|
-
end
|
100
|
-
end
|
101
|
-
end
|
102
|
-
|
103
|
-
def self.display data, type='nodal'
|
104
|
-
data = {'local' => data} if type == 'local'
|
105
|
-
ERB.new(File.open(@@template).read).result(binding)
|
106
|
-
end
|
107
|
-
|
108
|
-
def self.get_pg_connections
|
109
|
-
info = ActiveRecord::Base.connection.execute("SELECT datname,"\
|
110
|
-
"application_name,"\
|
111
|
-
"state,"\
|
112
|
-
"pid,"\
|
113
|
-
"client_addr "\
|
114
|
-
"FROM pg_stat_activity")
|
115
|
-
info.each_with_object({}) do |x, h|
|
116
|
-
h[x["datname"]] ||= []
|
117
|
-
h[x["datname"]] << {"name" => x["application_name"],
|
118
|
-
"address"=> x["client_addr"],
|
119
|
-
"state" => x["state"],
|
120
|
-
"pid" => x["pid"]}
|
121
|
-
end
|
122
|
-
end
|
123
|
-
|
124
|
-
def self.resolve_target_nodes target
|
125
|
-
db = ActiveRecord::Base.connection_config[:database]
|
126
|
-
db_conns = get_pg_connections
|
127
|
-
target_conns = db_conns[db].select{|x|
|
128
|
-
x['name'].include? target}
|
129
|
-
target_conns.map{|x| x['address']}.uniq.compact
|
130
|
-
end
|
131
|
-
|
132
|
-
def self.get_nodes
|
133
|
-
nodes = resolve_target_nodes("Passenger")
|
134
|
-
nodes.empty? ? ['127.0.0.1'] : nodes
|
135
|
-
end
|
136
|
-
end
|
137
|
-
############################################################################
|
138
|
-
#
|
139
|
-
# Diagnostic Definitions
|
140
|
-
# Default: pulls from all nodes; force local with '&scope=local'
|
141
|
-
#
|
142
|
-
############################################################################
|
143
|
-
class Version < Base
|
144
|
-
def self.generate
|
145
|
-
begin
|
146
|
-
message = `cd #{Rails.root.to_s}; git describe --tags --always;`.strip
|
147
|
-
rescue
|
148
|
-
message = error("Failed accessing git")
|
149
|
-
end
|
150
|
-
{
|
151
|
-
'Marty' => Marty::VERSION,
|
152
|
-
'Delorean' => Delorean::VERSION,
|
153
|
-
'Mcfly' => Mcfly::VERSION,
|
154
|
-
'Git' => message,
|
155
|
-
}
|
156
|
-
end
|
157
|
-
end
|
158
|
-
|
159
|
-
class Database < Base
|
160
|
-
def self.db_server_name
|
161
|
-
ActiveRecord::Base.connection_config[:host] || error('undefined')
|
162
|
-
end
|
163
|
-
|
164
|
-
def self.db_adapter_name
|
165
|
-
ActiveRecord::Base.connection.adapter_name
|
166
|
-
end
|
167
|
-
|
168
|
-
def self.db_time
|
169
|
-
ActiveRecord::Base.connection.execute('SELECT NOW();')
|
170
|
-
end
|
171
|
-
|
172
|
-
def self.db_version
|
173
|
-
begin
|
174
|
-
message = ActiveRecord::Base.connection.
|
175
|
-
execute('SELECT VERSION();')[0]['version']
|
176
|
-
rescue => e
|
177
|
-
return error(message)
|
178
|
-
end
|
179
|
-
message
|
180
|
-
end
|
181
|
-
|
182
|
-
def self.db_schema
|
183
|
-
begin
|
184
|
-
current = ActiveRecord::Migrator.current_version
|
185
|
-
needs_migration = ActiveRecord::Migrator.needs_migration?
|
186
|
-
rescue => e
|
187
|
-
return error(e.message)
|
188
|
-
end
|
189
|
-
needs_migration ? error("Migration is needed.\n"\
|
190
|
-
"Current Version: #{current}") : current
|
191
|
-
end
|
192
|
-
end
|
193
|
-
|
194
|
-
class Environment < Database
|
195
|
-
def self.generate
|
196
|
-
rbv = "#{RUBY_VERSION}-p#{RUBY_PATCHLEVEL} (#{RUBY_PLATFORM})"
|
197
|
-
{
|
198
|
-
'Environment' => Rails.env,
|
199
|
-
'Rails' => Rails.version,
|
200
|
-
'Netzke Core' => Netzke::Core::VERSION,
|
201
|
-
'Netzke Basepack' => Netzke::Basepack::VERSION,
|
202
|
-
'Ruby' => rbv,
|
203
|
-
'RubyGems' => Gem::VERSION,
|
204
|
-
'Database Adapter' => db_adapter_name,
|
205
|
-
'Database Server' => db_server_name,
|
206
|
-
'Database Version' => db_version,
|
207
|
-
'Database Schema Version' => db_schema
|
208
|
-
}
|
209
|
-
end
|
210
|
-
end
|
211
|
-
|
212
|
-
class Nodes < Base
|
213
|
-
def self.generate
|
214
|
-
begin
|
215
|
-
a_nodes = AwsInstanceInfo.new.nodes.sort if AwsInstanceInfo.is_aws?
|
216
|
-
rescue => e
|
217
|
-
a_nodes = [e.message]
|
218
|
-
end
|
219
|
-
pg_nodes = get_nodes.sort
|
220
|
-
message = a_nodes.nil? || pg_nodes == a_nodes ? pg_nodes.join("\n") :
|
221
|
-
error("There is a discrepancy between nodes connected to "\
|
222
|
-
"Postgres and those discovered through AWS EC2.\n"\
|
223
|
-
"Postgres: \n#{pg_nodes.join("\n")}\n"\
|
224
|
-
"AWS: \n#{a_nodes.join("\n")}")
|
225
|
-
{"PG/AWS" => message}
|
226
|
-
end
|
227
|
-
end
|
228
|
-
|
229
|
-
class Env < Base
|
230
|
-
def self.filter_env filter=''
|
231
|
-
env = ENV.clone
|
232
|
-
|
233
|
-
# obfuscate SECRET_KEY_BASE for comparison
|
234
|
-
env['SECRET_KEY_BASE'] = env['SECRET_KEY_BASE'][0,4] if
|
235
|
-
env['SECRET_KEY_BASE']
|
236
|
-
|
237
|
-
# remove SCRIPT_URI, SCRIPT_URL as calling node differs
|
238
|
-
['SCRIPT_URI', 'SCRIPT_URL'].each{|k| env.delete(k)}
|
239
|
-
|
240
|
-
to_block = ['PASSWORD', 'DEBUG']
|
241
|
-
env.sort.each_with_object({}){|(k,v),h|
|
242
|
-
h[k] = v if to_block.all?{|b| !k.include?(b)} && k.include?(filter)}
|
243
|
-
end
|
244
|
-
|
245
|
-
def self.generate
|
246
|
-
filter_env
|
247
|
-
end
|
248
|
-
|
249
|
-
def self.aggregate
|
250
|
-
envs = get_nodal_diags(name.demodulize)
|
251
|
-
diff(envs) ? envs : package({})
|
252
|
-
end
|
253
|
-
end
|
254
|
-
|
255
|
-
class DelayedJob < Base
|
256
|
-
def self.delayed_job_count
|
257
|
-
db = ActiveRecord::Base.connection_config[:database]
|
258
|
-
get_pg_connections[db].count{|c| c['pid'] if
|
259
|
-
c['name'].include?('delayed_job')}
|
260
|
-
end
|
261
|
-
|
262
|
-
def self.validate data
|
263
|
-
data.each_with_object({}) do
|
264
|
-
|(k,v), h|
|
265
|
-
h[k] = v.count > 1 ? error("\n" + v.join("\n")) :
|
266
|
-
v[0] != ENV['DELAYED_VER'] ? error(v[0]) : v[0]
|
267
|
-
end
|
268
|
-
end
|
269
|
-
|
270
|
-
def self.generate
|
271
|
-
count = delayed_job_count
|
272
|
-
return {'Issue' => ['No delayed jobs are running.']} if count.zero?
|
273
|
-
|
274
|
-
# we will only iterate by half of the total delayed workers to avoid
|
275
|
-
# excess use of delayed job time
|
276
|
-
count = (count/2).zero? ? 1 : count/2
|
277
|
-
|
278
|
-
d_engine = Marty::ScriptSet.new.get_engine("Diagnostics")
|
279
|
-
res = d_engine.evaluate('VersionDelay', 'result', {'count' => count-1})
|
280
|
-
|
281
|
-
# merge results and remove duplicates
|
282
|
-
res.each_with_object({}){
|
283
|
-
|r, h|
|
284
|
-
h[r[0]] ||= []
|
285
|
-
h[r[0]] << r[1]
|
286
|
-
}.each_with_object({}){|(k,v), h| h[k] = v.uniq}
|
287
|
-
end
|
288
|
-
|
289
|
-
def self.aggregate
|
290
|
-
package(validate(generate))
|
291
|
-
end
|
292
|
-
|
293
|
-
def self.diff data
|
294
|
-
data = data[name.demodulize] if data[name.demodulize]
|
295
|
-
data.keys.map{|k| data[k]}.flatten.uniq.count != 1 ||
|
296
|
-
data[data.keys[0]] != ENV['DELAYED_VER']
|
297
|
-
end
|
298
|
-
end
|
299
|
-
|
300
|
-
############################################################################
|
301
|
-
#
|
302
|
-
# Reports
|
303
|
-
#
|
304
|
-
############################################################################
|
305
|
-
class Report < Base
|
306
|
-
class << self
|
307
|
-
attr_accessor :diags
|
308
|
-
end
|
309
|
-
|
310
|
-
def diags
|
311
|
-
self.class.diags
|
312
|
-
end
|
313
|
-
|
314
|
-
self.diags = ['nodes', 'version', 'environment']
|
315
|
-
|
316
|
-
def self.get_diag_klass diag
|
317
|
-
controller = name.split(name.demodulize)[0].constantize
|
318
|
-
controller.const_get(diag.capitalize)
|
319
|
-
end
|
320
|
-
|
321
|
-
def self.generate
|
322
|
-
diags.each_with_object({}){|d, h| h[d] = get_diag_klass(d).generate}
|
323
|
-
end
|
324
|
-
|
325
|
-
def self.aggregate
|
326
|
-
diags.each_with_object({}){|d, h| h[d] = get_diag_klass(d).aggregate}
|
327
|
-
end
|
328
|
-
|
329
|
-
def self.display data, type
|
330
|
-
report = '<h3>' +
|
331
|
-
name.demodulize +
|
332
|
-
" #{'(' + type + ')' if type == 'local'}" +
|
333
|
-
'</h3>'
|
334
|
-
displays = diags.map{|d| get_diag_klass(d).display(data[d], type)}
|
335
|
-
([report] + displays).sum
|
336
|
-
end
|
337
|
-
end
|
338
|
-
|
339
|
-
############################################################################
|
340
|
-
#
|
341
|
-
# AWS Helper Class
|
342
|
-
#
|
343
|
-
############################################################################
|
344
|
-
class AwsInstanceInfo
|
345
|
-
attr_accessor :id, :doc, :role, :creds, :version, :host, :tag, :nodes
|
346
|
-
|
347
|
-
# aws reserved host used to get instance meta-data
|
348
|
-
META_DATA_HOST = '169.254.169.254'
|
349
|
-
|
350
|
-
def self.is_aws?
|
351
|
-
uri = URI.parse("http://#{META_DATA_HOST}")
|
352
|
-
!(Net::HTTP.get(uri) rescue nil).nil?
|
353
|
-
end
|
354
|
-
|
355
|
-
def initialize
|
356
|
-
@id = get_instance_id
|
357
|
-
@doc = get_document
|
358
|
-
@role = get_role
|
359
|
-
@creds = get_credentials
|
360
|
-
@host = "ec2.#{@doc['region']}.amazonaws.com"
|
361
|
-
@version = '2016-11-15'
|
362
|
-
@tag = get_tag
|
363
|
-
@nodes = get_private_ips
|
364
|
-
end
|
365
|
-
|
366
|
-
private
|
367
|
-
def query_meta_data query
|
368
|
-
uri = URI.parse("http://#{META_DATA_HOST}/latest/meta-data/#{query}/")
|
369
|
-
Net::HTTP.get(uri)
|
370
|
-
end
|
371
|
-
|
372
|
-
def query_dynamic query
|
373
|
-
uri = URI.parse("http://#{META_DATA_HOST}/latest/dynamic/#{query}/")
|
374
|
-
Net::HTTP.get(uri)
|
375
|
-
end
|
376
|
-
|
377
|
-
def get_instance_id
|
378
|
-
query_meta_data('instance-id').to_s
|
379
|
-
end
|
380
|
-
|
381
|
-
def get_role
|
382
|
-
query_meta_data('iam/security-credentials').to_s
|
383
|
-
end
|
384
|
-
|
385
|
-
def get_credentials
|
386
|
-
JSON.parse(query_meta_data("iam/security-credentials/#{@role}"))
|
387
|
-
end
|
388
|
-
|
389
|
-
def get_document
|
390
|
-
JSON.parse(query_dynamic('instance-identity/document'))
|
391
|
-
end
|
392
|
-
|
393
|
-
def ec2_req action, params = {}
|
394
|
-
url = "https://#{@host}/?Action=#{action}&Version=#{@version}"
|
395
|
-
params.each{|a, v| url += "&#{a}=#{v}"}
|
396
|
-
|
397
|
-
sig = Aws::Sigv4::Signer.new(service: 'ec2',
|
398
|
-
region: @doc['region'],
|
399
|
-
access_key_id: @creds['AccessKeyId'],
|
400
|
-
secret_access_key: @creds['SecretAccessKey'],
|
401
|
-
session_token: @creds['Token'])
|
402
|
-
signed_url = sig.presign_url(http_method:'GET', url: url)
|
403
|
-
|
404
|
-
http = Net::HTTP.new(@host, 443)
|
405
|
-
http.use_ssl = true
|
406
|
-
Hash.from_xml(Net::HTTP.get(signed_url))["#{action}Response"]
|
407
|
-
end
|
408
|
-
|
409
|
-
def get_tag
|
410
|
-
params = {'Filter.1.Name' => 'resource-id',
|
411
|
-
'Filter.1.Value.1' => get_instance_id,
|
412
|
-
'Filter.2.Name' => 'key',
|
413
|
-
'Filter.2.Value.1' => 'Name'}
|
414
|
-
ec2_req('DescribeTags', params)['tagSet']['item']['value']
|
415
|
-
end
|
416
|
-
|
417
|
-
def get_instances
|
418
|
-
params = {'Filter.1.Name' => 'tag-value',
|
419
|
-
'Filter.1.Value.1' => @tag}
|
420
|
-
ec2_req('DescribeInstances', params)
|
421
|
-
end
|
422
|
-
|
423
|
-
def get_private_ips
|
424
|
-
get_instances['reservationSet']['item'].map{
|
425
|
-
|i|
|
426
|
-
item = i['instancesSet']['item']
|
427
|
-
item.is_a?(Array) ? item.map{|i| i['privateIpAddress']} :
|
428
|
-
item['privateIpAddress']
|
429
|
-
}.flatten
|
430
|
-
end
|
22
|
+
def display_parameters
|
23
|
+
local = params[:scope] == 'local'
|
24
|
+
data = local ? @result : @result['data']
|
25
|
+
errors = local ? Diagnostic::Reporter.errors(data) : @result['errors']
|
26
|
+
{
|
27
|
+
'display' => Diagnostic::Reporter.displays(data),
|
28
|
+
'errors' => errors
|
29
|
+
}
|
431
30
|
end
|
432
31
|
end
|
433
32
|
end
|