marty 1.0.44 → 1.0.46
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 +3 -0
- data/Gemfile.lock +4 -2
- data/app/controllers/marty/diagnostic_controller.rb +324 -72
- data/app/controllers/marty/rpc_controller.rb +10 -4
- data/app/models/marty/data_grid.rb +47 -95
- data/app/views/marty/diagnostic/op.html.erb +58 -0
- data/config/routes.rb +1 -2
- data/db/js/errinfo_v1.js +16 -0
- data/db/js/lookup_grid_distinct_v1.js +64 -0
- data/db/js/query_grid_dir_v1.js +61 -0
- data/db/migrate/400_create_dg_plv8_v1_fns.rb +12 -0
- data/lib/marty/migrations.rb +20 -0
- data/lib/marty/schema_helper.rb +41 -0
- data/lib/marty/version.rb +1 -1
- data/spec/controllers/diagnostic_controller_spec.rb +157 -67
- data/spec/controllers/rpc_controller_spec.rb +45 -10
- data/spec/dummy/public/{404.html → 400.html} +0 -0
- data/spec/lib/logger_spec.rb +1 -0
- data/spec/models/data_grid_spec.rb +0 -1
- metadata +8 -4
- data/app/views/marty/diagnostic/diagnostic.html.erb +0 -15
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0437c9ffb81afc74c8bd834226b6855064e0d8bf
|
4
|
+
data.tar.gz: 27f841b78da83b42493bb74e5a6d3ee5944ee3b0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1ab061544b74d83b417815f07a85e3e8ee106a9ee03da67b0e54974b749dfdcb3a57387bc1e60f55a62fefc994ec15e5c59228d4ebe58e11999da2a7d9e816fb
|
7
|
+
data.tar.gz: a5466cb70f5908c1c2a93cf96b938fa10815f8b2683d0502a1293a04f75b943796f9bc0518ca180d2fdfe70eb3efcfdb2fe70f9353bf2e913a1e8ac14c6cdd17
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
marty (1.0.
|
4
|
+
marty (1.0.46)
|
5
5
|
axlsx (= 2.1.0pre)
|
6
6
|
coderay
|
7
7
|
delorean_lang (~> 0.1)
|
@@ -54,6 +54,7 @@ GEM
|
|
54
54
|
addressable (2.5.2)
|
55
55
|
public_suffix (>= 2.0.2, < 4.0)
|
56
56
|
arel (6.0.4)
|
57
|
+
aws-sigv4 (1.0.2)
|
57
58
|
axlsx (2.1.0.pre)
|
58
59
|
htmlentities (~> 4.3.1)
|
59
60
|
nokogiri (>= 1.4.1)
|
@@ -81,7 +82,7 @@ GEM
|
|
81
82
|
delayed_job_active_record (4.1.2)
|
82
83
|
activerecord (>= 3.0, < 5.2)
|
83
84
|
delayed_job (>= 3.0, < 5)
|
84
|
-
delorean_lang (0.3.
|
85
|
+
delorean_lang (0.3.32)
|
85
86
|
activerecord (>= 3.2)
|
86
87
|
treetop (~> 1.5)
|
87
88
|
diff-lcs (1.3)
|
@@ -205,6 +206,7 @@ PLATFORMS
|
|
205
206
|
ruby
|
206
207
|
|
207
208
|
DEPENDENCIES
|
209
|
+
aws-sigv4 (~> 1.0, >= 1.0.2)
|
208
210
|
capybara
|
209
211
|
daemons (~> 1.1.9)
|
210
212
|
database_cleaner
|
@@ -1,101 +1,353 @@
|
|
1
|
+
require 'erb'
|
1
2
|
module Marty
|
2
3
|
class DiagnosticController < ActionController::Base
|
3
|
-
layout
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
4
|
+
layout false
|
5
|
+
def op
|
6
|
+
begin
|
7
|
+
# inject request into Base class of all diagnostics
|
8
|
+
Base.request = request
|
9
|
+
params[:scope] = 'nodal' unless params[:scope]
|
10
|
+
diag = self.class.get_sub_class(params[:op])
|
11
|
+
@result = params[:scope] == 'local' ? diag.generate : diag.aggregate
|
12
|
+
rescue NameError
|
13
|
+
render file: 'public/400', formats: [:html], status: 400, layout: false
|
10
14
|
else
|
11
|
-
|
15
|
+
respond_to do |format|
|
16
|
+
format.html {@result = diag.display(@result, params[:scope])}
|
17
|
+
format.json {render json: @result}
|
18
|
+
end
|
12
19
|
end
|
13
20
|
end
|
14
21
|
|
15
|
-
def
|
16
|
-
|
17
|
-
[Diagnostic.new('Marty Version', true, VERSION)]
|
22
|
+
def self.get_sub_class klass
|
23
|
+
const_get(klass.downcase.camelize)
|
18
24
|
end
|
19
25
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
ActiveRecord::Base.connection.adapter_name)
|
33
|
-
]
|
34
|
-
begin
|
35
|
-
status = true
|
36
|
-
result = ActiveRecord::Base.connection.execute('SELECT VERSION();')
|
37
|
-
message = result[0]['version'] if result
|
38
|
-
rescue => e
|
39
|
-
status = false
|
40
|
-
message = e.message
|
26
|
+
private
|
27
|
+
############################################################################
|
28
|
+
#
|
29
|
+
# Diagnostics
|
30
|
+
#
|
31
|
+
############################################################################
|
32
|
+
class Base
|
33
|
+
@@request = nil
|
34
|
+
@@read_only = Marty::Util.db_in_recovery?
|
35
|
+
|
36
|
+
def self.request= req
|
37
|
+
@@request = req
|
41
38
|
end
|
42
|
-
details << Diagnostic.new('Database Version', status, message)
|
43
39
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
40
|
+
def self.request
|
41
|
+
@@request
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.aggregate op_name=name.demodulize
|
45
|
+
get_nodal_diags(op_name)
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.get_nodal_diags op_name, scope='local'
|
49
|
+
self.get_nodes.map do |n|
|
50
|
+
ssl = ENV['HTTPS'] == 'on'
|
51
|
+
uri = Addressable::URI.new(host: n, port: ssl ? 443 : @@request.port)
|
52
|
+
uri.query_values = {op: op_name.underscore,
|
53
|
+
scope: scope}
|
54
|
+
uri.scheme = ssl ? 'https' : 'http'
|
55
|
+
uri.path = '/marty/diag.json'
|
56
|
+
opts = {ssl_verify_mode: OpenSSL::SSL::VERIFY_NONE}
|
57
|
+
{n => JSON.parse(open(uri, opts).readlines[0])}
|
58
|
+
end.sum
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.errors data
|
62
|
+
data.keys.count{|n| is_failure?(data[n])}
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.diff data
|
66
|
+
data.keys.map{|n| data[n]}.uniq.length != 1
|
67
|
+
end
|
68
|
+
|
69
|
+
def self.package message
|
70
|
+
{name.demodulize => message}
|
71
|
+
end
|
72
|
+
|
73
|
+
def self.is_failure? message
|
74
|
+
message.to_s.include?('Failure')
|
75
|
+
end
|
76
|
+
|
77
|
+
def self.error message
|
78
|
+
"Failure: (#{message})"
|
79
|
+
end
|
80
|
+
|
81
|
+
def self.display data, type='nodal'
|
82
|
+
data = {'local' => data} if type == 'local'
|
83
|
+
display = <<-ERB
|
84
|
+
<% inconsistent = diff(data) %>
|
85
|
+
<h3><%=name.demodulize%></h3>
|
86
|
+
<%='<h3 class="error">⚠ Issues Detected</h3>' if
|
87
|
+
inconsistent%>
|
88
|
+
<div class="wrapper">
|
89
|
+
<% data.each do |node, result| %>
|
90
|
+
<table>
|
91
|
+
<% issues = ('error' if inconsistent) %>
|
92
|
+
<th class="<%=issues%>"><%=inconsistent ? node :
|
93
|
+
'<small>consistent</small>'%></th>
|
94
|
+
<th class="<%=issues%>"></th>
|
95
|
+
<% result.each do |name, value| %>
|
96
|
+
<tr class="<%=is_failure?(value) ? 'failed' :
|
97
|
+
'passed' %>">
|
98
|
+
<td><%=name%></td>
|
99
|
+
<td class="overflow"><%=value%></td>
|
100
|
+
</tr>
|
101
|
+
<% end %>
|
102
|
+
</table>
|
103
|
+
<% break unless inconsistent %>
|
104
|
+
<% end %>
|
105
|
+
</div>
|
106
|
+
ERB
|
107
|
+
ERB.new(display.html_safe).result(binding)
|
108
|
+
end
|
109
|
+
|
110
|
+
def self.get_pg_connections
|
111
|
+
info = ActiveRecord::Base.connection.execute("SELECT datname,"\
|
112
|
+
"application_name,"\
|
113
|
+
"state,"\
|
114
|
+
"pid,"\
|
115
|
+
"client_addr "\
|
116
|
+
"FROM pg_stat_activity")
|
117
|
+
info.each_with_object({}) do |x, h|
|
118
|
+
h[x["datname"]] ||= []
|
119
|
+
h[x["datname"]] << {"name" => x["application_name"],
|
120
|
+
"address"=> x["client_addr"],
|
121
|
+
"state" => x["state"],
|
122
|
+
"pid" => x["pid"]}
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def self.resolve_target_nodes target
|
127
|
+
db = ActiveRecord::Base.connection_config[:database]
|
128
|
+
db_conns = get_pg_connections
|
129
|
+
target_conns = db_conns[db].select{|x|
|
130
|
+
x['name'].include? target}
|
131
|
+
target_conns.map{|x| x['address']}.uniq.compact
|
132
|
+
end
|
133
|
+
|
134
|
+
def self.get_nodes
|
135
|
+
nodes = resolve_target_nodes("Passenger")
|
136
|
+
nodes.empty? ? ['127.0.0.1'] : nodes
|
137
|
+
end
|
138
|
+
end
|
139
|
+
############################################################################
|
140
|
+
#
|
141
|
+
# Diagnostic Definitions
|
142
|
+
# Default: pulls from all nodes; force local with '&scope=local'
|
143
|
+
#
|
144
|
+
############################################################################
|
145
|
+
class Version < Base
|
146
|
+
def self.generate
|
147
|
+
begin
|
148
|
+
message = `cd #{Rails.root.to_s}; git describe;`.strip
|
149
|
+
rescue
|
150
|
+
message = error("Failed accessing git")
|
151
|
+
end
|
152
|
+
{
|
153
|
+
'Git' => message,
|
154
|
+
'Marty' => Marty::VERSION,
|
155
|
+
'Delorean' => Delorean::VERSION,
|
156
|
+
'Mcfly' => Mcfly::VERSION
|
157
|
+
}
|
49
158
|
end
|
50
|
-
details << Diagnostic.new('Database Schema Version', status, message)
|
51
|
-
diag_response details
|
52
159
|
end
|
53
160
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
161
|
+
class Database < Base
|
162
|
+
def self.db_server_name
|
163
|
+
ActiveRecord::Base.connection_config[:host] || 'undefined'
|
164
|
+
end
|
165
|
+
|
166
|
+
def self.db_adapter_name
|
167
|
+
ActiveRecord::Base.connection.adapter_name
|
168
|
+
end
|
169
|
+
|
170
|
+
def self.db_time
|
171
|
+
ActiveRecord::Base.connection.execute('SELECT NOW();')
|
172
|
+
end
|
173
|
+
|
174
|
+
def self.db_version
|
175
|
+
begin
|
176
|
+
message = ActiveRecord::Base.connection.
|
177
|
+
execute('SELECT VERSION();')[0]['version']
|
178
|
+
rescue => e
|
179
|
+
return error(message)
|
180
|
+
end
|
181
|
+
message
|
182
|
+
end
|
183
|
+
|
184
|
+
def self.db_schema
|
185
|
+
begin
|
186
|
+
message = ActiveRecord::Migrator.current_version
|
187
|
+
rescue => e
|
188
|
+
return error(e.message)
|
64
189
|
end
|
190
|
+
message
|
65
191
|
end
|
66
192
|
end
|
67
193
|
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
194
|
+
class Environment < Database
|
195
|
+
def self.generate
|
196
|
+
rbv = "#{RUBY_VERSION}-p#{RUBY_PATCHLEVEL} (#{RUBY_PLATFORM})"
|
197
|
+
infos = {'Environment' => Rails.env,
|
198
|
+
'Rails' => Rails.version,
|
199
|
+
'Netzke Core' => Netzke::Core::VERSION,
|
200
|
+
'Netzke Basepack' => Netzke::Basepack::VERSION,
|
201
|
+
'Ruby' => rbv,
|
202
|
+
'RubyGems' => Gem::VERSION,
|
203
|
+
'Database Adapter' => db_adapter_name,
|
204
|
+
'Database Server' => db_server_name,
|
205
|
+
'Database Version' => db_version,
|
206
|
+
'Database Schema Version' => db_schema}
|
76
207
|
end
|
77
208
|
end
|
78
209
|
|
79
|
-
|
80
|
-
|
210
|
+
class Nodes < Base
|
211
|
+
def self.generate
|
212
|
+
a_nodes = AwsInstanceInfo.is_aws? ? AwsInstanceInfo.new.nodes.sort : []
|
213
|
+
pg_nodes = get_nodes.sort
|
214
|
+
message = pg_nodes == a_nodes ? pg_nodes.join(', ') :
|
215
|
+
error("Postgres: [#{pg_nodes.join(', ')}]"\
|
216
|
+
" - AWS: [#{a_nodes.join(', ')}]")
|
217
|
+
{"PG/AWS" => message}
|
218
|
+
end
|
219
|
+
|
220
|
+
def self.aggregate
|
221
|
+
{'local' => generate}
|
222
|
+
end
|
81
223
|
end
|
82
224
|
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
225
|
+
############################################################################
|
226
|
+
#
|
227
|
+
# Reports
|
228
|
+
#
|
229
|
+
############################################################################
|
230
|
+
class Report < Base
|
231
|
+
class << self
|
232
|
+
attr_accessor :diags
|
233
|
+
end
|
234
|
+
|
235
|
+
def diags
|
236
|
+
self.class.diags
|
237
|
+
end
|
238
|
+
|
239
|
+
self.diags = ['nodes', 'version', 'environment']
|
240
|
+
|
241
|
+
def self.get_diag_klass diag
|
242
|
+
controller = name.split(name.demodulize)[0].constantize
|
243
|
+
controller.const_get(diag.capitalize)
|
244
|
+
end
|
245
|
+
|
246
|
+
def self.generate
|
247
|
+
diags.each_with_object({}){|d, h| h[d] = get_diag_klass(d).generate}
|
248
|
+
end
|
249
|
+
|
250
|
+
def self.aggregate
|
251
|
+
diags.each_with_object({}){|d, h| h[d] = get_diag_klass(d).aggregate}
|
252
|
+
end
|
253
|
+
|
254
|
+
def self.display data, type
|
255
|
+
report = '<h3>' +
|
256
|
+
name.demodulize +
|
257
|
+
" #{'(' + type + ')' if type == 'local'}" +
|
258
|
+
'</h3>'
|
259
|
+
displays = diags.map{|d| get_diag_klass(d).display(data[d], type)}
|
260
|
+
([report] + displays).sum
|
261
|
+
end
|
90
262
|
end
|
91
263
|
|
92
|
-
|
93
|
-
|
94
|
-
|
264
|
+
############################################################################
|
265
|
+
#
|
266
|
+
# AWS Helper Class
|
267
|
+
#
|
268
|
+
############################################################################
|
269
|
+
class AwsInstanceInfo
|
270
|
+
attr_accessor :id, :doc, :role, :creds, :version, :host, :tag, :nodes
|
271
|
+
|
272
|
+
# aws reserved host used to get instance meta-data
|
273
|
+
META_DATA_HOST = '169.254.169.254'
|
274
|
+
|
275
|
+
def self.is_aws?
|
276
|
+
uri = URI.parse("http://#{META_DATA_HOST}")
|
277
|
+
!(Net::HTTP.get(uri) rescue nil).nil?
|
278
|
+
end
|
279
|
+
|
280
|
+
def initialize
|
281
|
+
@id = get_instance_id
|
282
|
+
@doc = get_document
|
283
|
+
@role = get_role
|
284
|
+
@creds = get_credentials
|
285
|
+
@host = "ec2.#{@doc['region']}.amazonaws.com"
|
286
|
+
@version = '2016-11-15'
|
287
|
+
@tag = get_tag
|
288
|
+
@nodes = get_private_ips
|
289
|
+
end
|
290
|
+
|
291
|
+
private
|
292
|
+
def query_meta_data query
|
293
|
+
uri = URI.parse("http://#{META_DATA_HOST}/latest/meta-data/#{query}/")
|
294
|
+
Net::HTTP.get(uri)
|
295
|
+
end
|
296
|
+
|
297
|
+
def query_dynamic query
|
298
|
+
uri = URI.parse("http://#{META_DATA_HOST}/latest/dynamic/#{query}/")
|
299
|
+
Net::HTTP.get(uri)
|
300
|
+
end
|
301
|
+
|
302
|
+
def get_instance_id
|
303
|
+
query_meta_data('instance-id').to_s
|
304
|
+
end
|
305
|
+
|
306
|
+
def get_role
|
307
|
+
query_meta_data('iam/security-credentials').to_s
|
308
|
+
end
|
309
|
+
|
310
|
+
def get_credentials
|
311
|
+
JSON.parse(query_meta_data("iam/security-credentials/#{@role}"))
|
312
|
+
end
|
313
|
+
|
314
|
+
def get_document
|
315
|
+
JSON.parse(query_dynamic('instance-identity/document'))
|
316
|
+
end
|
317
|
+
|
318
|
+
def ec2_req action, params = {}
|
319
|
+
url = "https://#{@host}/?Action=#{action}&Version=#{@version}"
|
320
|
+
params.each{|a, v| url += "&#{a}=#{v}"}
|
321
|
+
|
322
|
+
sig = Aws::Sigv4::Signer.new(service: 'ec2',
|
323
|
+
region: @doc['region'],
|
324
|
+
access_key_id: @creds['AccessKeyId'],
|
325
|
+
secret_access_key: @creds['SecretAccessKey'],
|
326
|
+
session_token: @creds['Token'])
|
327
|
+
signed_url = sig.presign_url(http_method:'GET', url: url)
|
328
|
+
|
329
|
+
http = Net::HTTP.new(@host, 443)
|
330
|
+
http.use_ssl = true
|
331
|
+
Hash.from_xml(Net::HTTP.get(signed_url))["#{action}Response"]
|
332
|
+
end
|
333
|
+
|
334
|
+
def get_tag
|
335
|
+
params = {'Filter.1.Name' => 'resource-id',
|
336
|
+
'Filter.1.Value.1' => get_instance_id,
|
337
|
+
'Filter.2.Name' => 'key',
|
338
|
+
'Filter.2.Value.1' => 'Name'}
|
339
|
+
ec2_req('DescribeTags', params)['tagSet']['item']['value']
|
340
|
+
end
|
341
|
+
|
342
|
+
def get_instances
|
343
|
+
params = {'Filter.1.Name' => 'tag-value',
|
344
|
+
'Filter.1.Value.1' => @tag}
|
345
|
+
ec2_req('DescribeInstances', params)
|
95
346
|
end
|
96
347
|
|
97
|
-
def
|
98
|
-
|
348
|
+
def get_private_ips
|
349
|
+
get_instances['reservationSet']['item'].
|
350
|
+
map{|i| i['instancesSet']['item']['privateIpAddress']}
|
99
351
|
end
|
100
352
|
end
|
101
353
|
end
|