marty 1.1.5 → 1.1.6
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/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
|