greenhat 0.1.4

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.
Files changed (70) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +64 -0
  3. data/bin/greenhat +12 -0
  4. data/lib/greenhat.rb +80 -0
  5. data/lib/greenhat/accessors/disk.rb +27 -0
  6. data/lib/greenhat/accessors/logs/production.rb +41 -0
  7. data/lib/greenhat/accessors/logs/sidekiq.rb +41 -0
  8. data/lib/greenhat/accessors/memory.rb +46 -0
  9. data/lib/greenhat/accessors/network.rb +8 -0
  10. data/lib/greenhat/accessors/process.rb +8 -0
  11. data/lib/greenhat/archive.rb +108 -0
  12. data/lib/greenhat/cli.rb +448 -0
  13. data/lib/greenhat/host.rb +182 -0
  14. data/lib/greenhat/logbot.rb +86 -0
  15. data/lib/greenhat/pry_helpers.rb +51 -0
  16. data/lib/greenhat/settings.rb +51 -0
  17. data/lib/greenhat/shell.rb +92 -0
  18. data/lib/greenhat/shell/cat.rb +125 -0
  19. data/lib/greenhat/shell/disk.rb +68 -0
  20. data/lib/greenhat/shell/faststats.rb +195 -0
  21. data/lib/greenhat/shell/gitlab.rb +45 -0
  22. data/lib/greenhat/shell/help.rb +15 -0
  23. data/lib/greenhat/shell/helper.rb +514 -0
  24. data/lib/greenhat/shell/log.rb +344 -0
  25. data/lib/greenhat/shell/memory.rb +31 -0
  26. data/lib/greenhat/shell/network.rb +12 -0
  27. data/lib/greenhat/shell/process.rb +12 -0
  28. data/lib/greenhat/shell/report.rb +319 -0
  29. data/lib/greenhat/thing.rb +121 -0
  30. data/lib/greenhat/thing/file_types.rb +705 -0
  31. data/lib/greenhat/thing/formatters/api_json.rb +34 -0
  32. data/lib/greenhat/thing/formatters/bracket_log.rb +44 -0
  33. data/lib/greenhat/thing/formatters/clean_raw.rb +23 -0
  34. data/lib/greenhat/thing/formatters/colon_split_strip.rb +12 -0
  35. data/lib/greenhat/thing/formatters/dotenv.rb +10 -0
  36. data/lib/greenhat/thing/formatters/format.rb +12 -0
  37. data/lib/greenhat/thing/formatters/free_m.rb +29 -0
  38. data/lib/greenhat/thing/formatters/gitlab_ctl_tail.rb +51 -0
  39. data/lib/greenhat/thing/formatters/gitlab_status.rb +26 -0
  40. data/lib/greenhat/thing/formatters/json.rb +63 -0
  41. data/lib/greenhat/thing/formatters/json_shellwords.rb +44 -0
  42. data/lib/greenhat/thing/formatters/multiline_json.rb +10 -0
  43. data/lib/greenhat/thing/formatters/raw.rb +18 -0
  44. data/lib/greenhat/thing/formatters/shellwords.rb +23 -0
  45. data/lib/greenhat/thing/formatters/table.rb +26 -0
  46. data/lib/greenhat/thing/formatters/time_json.rb +21 -0
  47. data/lib/greenhat/thing/formatters/time_shellwords.rb +28 -0
  48. data/lib/greenhat/thing/formatters/time_space.rb +36 -0
  49. data/lib/greenhat/thing/helpers.rb +71 -0
  50. data/lib/greenhat/thing/history.rb +51 -0
  51. data/lib/greenhat/thing/info_format.rb +193 -0
  52. data/lib/greenhat/thing/kind.rb +97 -0
  53. data/lib/greenhat/thing/spinner.rb +52 -0
  54. data/lib/greenhat/thing/super_log.rb +102 -0
  55. data/lib/greenhat/tty/custom_line.rb +29 -0
  56. data/lib/greenhat/tty/line.rb +326 -0
  57. data/lib/greenhat/tty/reader.rb +110 -0
  58. data/lib/greenhat/version.rb +3 -0
  59. data/lib/greenhat/views/css.slim +126 -0
  60. data/lib/greenhat/views/disk_free.slim +18 -0
  61. data/lib/greenhat/views/index.slim +51 -0
  62. data/lib/greenhat/views/info.slim +39 -0
  63. data/lib/greenhat/views/manifest.slim +22 -0
  64. data/lib/greenhat/views/memory.slim +18 -0
  65. data/lib/greenhat/views/netstat.slim +20 -0
  66. data/lib/greenhat/views/process.slim +21 -0
  67. data/lib/greenhat/views/systemctl.slim +40 -0
  68. data/lib/greenhat/views/ulimit.slim +15 -0
  69. data/lib/greenhat/web.rb +46 -0
  70. metadata +476 -0
@@ -0,0 +1,448 @@
1
+ # Top level namespace
2
+ module GreenHat
3
+ # CLI Methods
4
+ # rubocop:disable Metrics/ModuleLength
5
+ module Cli
6
+ def self.cursor
7
+ @cursor = TTY::Cursor
8
+ end
9
+
10
+ # Input Loop Listener
11
+ def self.reader
12
+ @reader ||= reader_setup
13
+ end
14
+
15
+ def self.files
16
+ @files ||= Thing.all.map(&:name).uniq
17
+ end
18
+
19
+ def self.reader_setup
20
+ reader = TTY::Reader.new(history_duplicates: false, interrupt: -> { back })
21
+
22
+ Settings.cmd_history_clean.each do |line|
23
+ reader.add_to_history(line)
24
+ end
25
+
26
+ # Blank?
27
+ reader.add_to_history ''
28
+
29
+ # Remove Line
30
+ reader.on(:keyctrl_u) do |_event|
31
+ reader.line.remove reader.line.text.size
32
+ end
33
+
34
+ # Navigate Word Left
35
+ reader.on(:keyctrl_left) { reader.line.move_word_left }
36
+
37
+ # Navigate Word Right
38
+ reader.on(:keyctrl_right) { reader.line.move_word_right }
39
+
40
+ # Navigate Beginning
41
+ reader.on(:keyctrl_a) { reader.line.move_to_start }
42
+
43
+ # Navigate End
44
+ reader.on(:keyctrl_e) { reader.line.move_to_end }
45
+
46
+ reader.on(:keyback_tab) { back }
47
+
48
+ reader.on(:keytab) do
49
+ process
50
+ auto
51
+ end
52
+
53
+ reader.instance_variable_get(:@history)
54
+
55
+ # DEBUG PRY
56
+ reader.on(:keyctrl_p) do |event|
57
+ binding.pry
58
+ end
59
+ reader
60
+ end
61
+
62
+ # =========================================================
63
+ # Auto Complete
64
+ # =========================================================
65
+ def self.auto
66
+ word = reader.line.word
67
+
68
+ if word.blank?
69
+ help
70
+ else
71
+ auto_match(current_methods + current_submodules + cmd_list, word)
72
+ end
73
+ end
74
+
75
+ def self.auto_match(matches, word)
76
+ matches.select! { |x| x[/^#{Regexp.escape(word)}/] }
77
+
78
+ if submodule?
79
+ submodule!
80
+ reader.breaker = true
81
+ # Only one Match!
82
+ elsif matches.count == 1
83
+ auto_update(matches.first, word)
84
+
85
+ # Print List of Options
86
+ elsif matches.count > 1
87
+ puts matches.join("\t").colorize(:light_green)
88
+
89
+ # No other Matches
90
+ else
91
+ file_matches = files.select { |x| x[/^#{Regexp.escape(word)}/] }
92
+ auto_files(file_matches, word) unless file_matches.empty?
93
+
94
+ end
95
+ end
96
+
97
+ # Auto Complete File Names
98
+ def self.auto_files(matches, word)
99
+ if matches.count == 1
100
+ auto_update(matches.first, word)
101
+
102
+ # Print List of Options
103
+ elsif matches.count > 1
104
+ auto_update(common_substr(matches), word)
105
+ puts matches.join("\t").colorize(:light_green)
106
+ end
107
+ end
108
+
109
+ # Handle Updates to Reader
110
+ def self.auto_update(match, word)
111
+ add = match.split(word, 2).last
112
+ reader.line.insert add unless add.nil?
113
+ end
114
+
115
+ # Complete as much as possible comparison
116
+ # https://stackoverflow.com/a/2158481/1678507
117
+ def self.common_substr(strings)
118
+ shortest = strings.min_by(&:length)
119
+ maxlen = shortest.length
120
+ maxlen.downto(0) do |len|
121
+ 0.upto(maxlen - len) do |start|
122
+ substr = shortest[start, len]
123
+ return substr if strings.all? { |str| str.include? substr }
124
+ end
125
+ end
126
+ end
127
+ # =========================================================
128
+
129
+ def self.did_you_mean
130
+ dictionary = current_methods + current_submodules + cmd_list
131
+
132
+ # all.select! { |x| x.include? cmd }
133
+
134
+ all = DidYouMean::SpellChecker.new(dictionary: dictionary).correct(cmd)
135
+
136
+ if all.empty?
137
+ puts [
138
+ 'Command not found '.colorize(:red),
139
+ cmd.colorize(:light_yellow),
140
+ ' ('.colorize(:light_black),
141
+ 'help'.colorize(:blue),
142
+ ' to show available commands'.colorize(:light_black),
143
+ ')'.colorize(:light_black)
144
+ ].join
145
+ else
146
+ puts "#{'Did you mean?'.colorize(:cyan)} #{all.join("\t").colorize(:green)}"
147
+ end
148
+ end
149
+
150
+ # Auto/Run Process - Populate and parse: @list
151
+ def self.process
152
+ line = reader.line
153
+ @list = Shellwords.split line.text
154
+ rescue StandardError => e
155
+ puts "#{'Invalid Command'.colorize(:red)}: #{e.message.colorize(:green)}"
156
+ end
157
+
158
+ def self.available(list)
159
+ list.map(&:to_s).map(&:downcase)
160
+ end
161
+
162
+ # Reader helper
163
+ def self.cmd
164
+ @list.first
165
+ end
166
+
167
+ def self.run?
168
+ current_methods.include? cmd
169
+ end
170
+
171
+ # Special Command Overrides
172
+ def self.cmd_list
173
+ ['help', '~', '..', 'back', 'clear']
174
+ end
175
+
176
+ def self.cmd?
177
+ cmd_list.any? cmd
178
+ end
179
+
180
+ def self.submodule?
181
+ current_submodules.include? @list.first
182
+ end
183
+
184
+ def self.cmd_run
185
+ case cmd
186
+ when 'help' then help
187
+ when '~' then home
188
+ when '..', 'back' then back
189
+ when 'clear' then clear_screen
190
+ end
191
+
192
+ @list.shift
193
+ end
194
+
195
+ # Final Command Execution
196
+ def self.run!
197
+ # Shift to prevent duplicate Runs / Arity for identifying if Method has params
198
+ if current_location.method(@list.first).arity.zero?
199
+ current_location.send(@list.shift.clone)
200
+ else
201
+ current_location.send(@list.shift.clone, @list.clone)
202
+ end
203
+ end
204
+
205
+ def self.run
206
+ return true if @list.blank?
207
+
208
+ if cmd?
209
+ cmd_run
210
+ elsif run?
211
+ run!
212
+
213
+ return true # End Early
214
+
215
+ elsif current_submodule?
216
+ @list.shift
217
+ elsif submodule?
218
+ submodule!
219
+ # Prepend Default if exists
220
+ elsif default?
221
+ @list.unshift 'default'
222
+ else
223
+ did_you_mean
224
+ @list.shift
225
+ end
226
+
227
+ run # Loop Back
228
+ rescue StandardError => e
229
+ LogBot.fatal('CLI Run', e.message)
230
+ puts e.backtrace[0..4].join("\n").colorize(:red)
231
+ end
232
+
233
+ # Check for `default` method and files
234
+ def self.default?
235
+ files.include?(cmd) && current_methods.include?('default')
236
+ end
237
+
238
+ # General Helper
239
+ def self.help
240
+ if current_location.methods(false).count.zero?
241
+ puts 'No Commands'.colorize(:red)
242
+ else
243
+ puts 'Commands: '
244
+ current_location.methods(false).map(&:to_s).sort.each do |item|
245
+ next if %w[default help].any? { |x| x == item }
246
+
247
+ puts "=> #{item.to_s.colorize(:blue)}"
248
+ end
249
+
250
+ current_location.send(:help) if current_methods.include? 'help'
251
+ end
252
+
253
+ puts ''
254
+
255
+ if current_location.constants.count.zero?
256
+ puts 'No Submodules'.colorize(:red)
257
+ else
258
+ puts 'Submodules'
259
+ current_location.constants.each do |item|
260
+ puts "-> #{item.to_s.demodulize.downcase.colorize(:yellow)}"
261
+ end
262
+ end
263
+ end
264
+
265
+ def self.location
266
+ @location ||= [Shell]
267
+ end
268
+
269
+ # Replace Location
270
+ def self.move(spot)
271
+ @location = [spot]
272
+ end
273
+
274
+ # Append to Location
275
+ def self.submodule!
276
+ spot = @list.shift
277
+ reader.line.replace @list.join(' ')
278
+ @location << current_location.const_get(spot.capitalize.to_sym)
279
+ end
280
+
281
+ def self.home
282
+ move Shell
283
+ end
284
+
285
+ def self.back
286
+ move location[-2] if location.count > 1
287
+ end
288
+
289
+ # Reader to Get Last Location's Available Methods / Keep Helper Methods
290
+ def self.current_methods
291
+ current_location.methods(false).map(&:to_s)
292
+ # .reject { |x| x.include? '_help' }
293
+ end
294
+
295
+ def self.current_submodules
296
+ current_location.constants.map(&:to_s).map(&:downcase)
297
+ end
298
+
299
+ # Full Commands at the Submodule `disk summary` from `disk`
300
+ def self.current_submodule?
301
+ current_location.to_s.demodulize.downcase == @list.first
302
+ end
303
+
304
+ def self.current_location
305
+ location.last
306
+ end
307
+
308
+ def self.location_reader
309
+ location.map(&:to_s).map(&:downcase).map(&:demodulize).join('/').gsub('shell', '~').colorize(:blue)
310
+ end
311
+
312
+ def self.readline_notch
313
+ "#{'greenhat'.colorize(:light_black)} #{location_reader} » "
314
+ end
315
+
316
+ def self.clear_screen
317
+ print cursor.clear_screen + cursor.move_to(0, 0)
318
+ end
319
+
320
+ def self.quiet
321
+ @quiet
322
+ end
323
+
324
+ # Process any Args
325
+ # --report
326
+ # --quiet
327
+ # --load -- Auto Load Things
328
+ # TODO: Add Help (--help)
329
+ def self.startup_args(files)
330
+ @report = true if files.any? { |x| ['--report', '-r'].include? x }
331
+ @quiet = true if files.any? { |x| ['--quiet', '-q'].include? x }
332
+ @load = true if files.any? { |x| ['--load', '-l'].include? x }
333
+
334
+ binding.pry if files.include? '--help'
335
+
336
+ files.reject! { |x| startup_arg_list.include? x }
337
+ end
338
+
339
+ def self.startup_arg_list
340
+ ['--load', '--l', '--quiet', '-q', '--report', '-r']
341
+ end
342
+
343
+ # rubocop:disable Metrics/MethodLength
344
+ def self.start(files)
345
+ # If no arguments Supplied Print and quit - rather than nasty exception
346
+ # TODO: Add Usage
347
+
348
+ Settings.start
349
+
350
+ startup_args files
351
+ clear_screen
352
+
353
+ if files.empty? || files.count { |x| File.exist? x }.zero?
354
+ puts "No arguments or files don't exist".colorize(:red)
355
+ puts 'Usage: greenhat <sos-archive.tgz> <sos-archive2.tgz>'
356
+ Shell.about
357
+ end
358
+
359
+ load_files files
360
+
361
+ # Handle Args
362
+ if @report
363
+ # Don't Use Pagination
364
+ GreenHat::Shell.report(['--raw'])
365
+ return true
366
+ end
367
+
368
+ Thing.all.each(&:process) if @load
369
+
370
+ value ||= '' # Empty Start
371
+
372
+ loop do
373
+ line = reader.read_line(readline_notch, value: value)
374
+ value = '' # Remove Afterwards
375
+
376
+ if reader.breaker
377
+ value = line
378
+ puts ''
379
+ next
380
+ end
381
+
382
+ if line =~ /^exit/i
383
+ Settings.cmd_write
384
+
385
+ break
386
+ end
387
+
388
+ Settings.cmd_add(line) unless line.blank?
389
+
390
+ process
391
+ run
392
+ end
393
+ end
394
+ # rubocop:enable Metrics/MethodLength
395
+
396
+ def self.load_files(files)
397
+ # TODO: Web Helpers?
398
+ # suppress_output { GreenHat::Web.start }
399
+
400
+ # Don't double up on archives / Only Existing files
401
+ puts 'Loading Archives'.colorize(:blue)
402
+ files.uniq.each do |file|
403
+ next unless File.exist?(file)
404
+
405
+ puts "- #{file}".colorize(:magenta)
406
+ ArchiveLoader.load file
407
+ end
408
+ end
409
+
410
+ def self.suppress_output
411
+ original_stderr = $stderr.clone
412
+ original_stdout = $stdout.clone
413
+ $stderr.reopen(File.new('/dev/null', 'w'))
414
+ $stdout.reopen(File.new('/dev/null', 'w'))
415
+ yield
416
+ ensure
417
+ $stdout.reopen(original_stdout)
418
+ $stderr.reopen(original_stderr)
419
+ end
420
+
421
+ def self.template
422
+ "#{__dir__}/views/index.slim"
423
+ end
424
+
425
+ def self.output
426
+ 'greenhat.html'
427
+ end
428
+
429
+ def self.prompt
430
+ TTY::Prompt.new(active_color: :cyan)
431
+ end
432
+
433
+ # TODO
434
+ def self.menu
435
+ prompt.select('Wat do?') do |menu|
436
+ menu.choice name: 'exit'
437
+ menu.choice name: 'Export Kibana Dashboard', value: 'export'
438
+ menu.choice name: 'console (pry)', value: 'console'
439
+ end
440
+ end
441
+
442
+ def self.bad_file(archive)
443
+ puts color("Cannot find archive: #{archive}", :red)
444
+ exit 1
445
+ end
446
+ end
447
+ # rubocop:enable Metrics/ModuleLength
448
+ end
@@ -0,0 +1,182 @@
1
+ # Individual Host Helper
2
+ class Host < Teron
3
+ include ActionView::Helpers::DateHelper
4
+ include ActionView::Helpers::NumberHelper
5
+
6
+ field :name
7
+
8
+ belongs_to :archive
9
+
10
+ # def load(file)
11
+ # @file = file
12
+ # self.files = Archive.load(file)
13
+ # save!
14
+ # end
15
+
16
+ def archive_name
17
+ File.basename archive
18
+ end
19
+
20
+ def find_file(file_name)
21
+ files.find { |x| x.name == file_name }
22
+ end
23
+
24
+ def icon
25
+ release_file = find_file files.map(&:name).grep(/_release/).first
26
+
27
+ release = release_file.raw.join
28
+
29
+ if release.include? 'ubuntu'
30
+ 'fa-ubuntu'
31
+ elsif release.include? 'suse'
32
+ 'fa-suse'
33
+ elsif release.include? 'redhat'
34
+ 'fa-redhat'
35
+ elsif release.include? 'centos'
36
+ 'fa-centos'
37
+ else
38
+ 'fa-linux'
39
+ end
40
+ end
41
+
42
+ # ----------------------------
43
+ # Helpers
44
+ # ----------------------------
45
+ def percent(value, total)
46
+ (value.to_i / total.to_f).round(2) * 100
47
+ end
48
+
49
+ def systemctl_color(entry)
50
+ case entry.status
51
+ when 'enabled' then :green
52
+ when 'static' then :orange
53
+ when 'disabled' then :red
54
+ else
55
+ :grey
56
+ end
57
+ end
58
+ # ---------------------------
59
+
60
+ def manifest
61
+ file = find_file 'gitlab_version_manifest_json'
62
+ return nil unless file
63
+
64
+ Oj.load file.raw.join
65
+ end
66
+
67
+ def uptime
68
+ file = find_file 'uptime'
69
+ return nil unless file
70
+
71
+ file.raw
72
+ end
73
+
74
+ def uname
75
+ file = find_file 'uname'
76
+ return nil unless file
77
+
78
+ file.raw.join
79
+ end
80
+
81
+ def cpuinfo
82
+ file = find_file 'cpuinfo'
83
+ return nil unless file
84
+
85
+ file.raw.join("\n").split("\n\n").map do |cpu|
86
+ all = cpu.split("\n").map do |row|
87
+ row.delete("\t").split(': ')
88
+ end
89
+ { details: all[1..], order: all[0].last }
90
+ end
91
+ end
92
+
93
+ def cpu_speed
94
+ file = find_file 'lscpu'
95
+ return nil unless file
96
+
97
+ details = file.raw.find { |x| x.include? 'MHz' }
98
+ details.reverse.split(' ', 2).map(&:reverse).reverse
99
+ end
100
+
101
+ def total_memory
102
+ file = find_file 'free_m'
103
+ return nil unless file
104
+
105
+ value = file.raw.dig(1, 1).to_i
106
+ number_to_human_size(value * 1024 * 1024)
107
+ end
108
+
109
+ def free_m
110
+ file = find_file 'free_m'
111
+ return nil unless file
112
+
113
+ file.raw
114
+ end
115
+
116
+ def df_h
117
+ file = find_file 'df_h'
118
+ return nil unless file
119
+
120
+ file.raw
121
+ end
122
+
123
+ def netstat
124
+ file = find_file 'netstat'
125
+ return nil unless file
126
+
127
+ file.raw
128
+ end
129
+
130
+ def ulimit
131
+ file = find_file 'ulimit'
132
+ return nil unless file
133
+
134
+ results = file.raw.map do |entry|
135
+ {
136
+ value: entry.split[-1],
137
+ details: entry.split(' ').first
138
+ }
139
+ end
140
+
141
+ results.sort_by { |x| x[:details].downcase }
142
+ end
143
+
144
+ def processes
145
+ file = find_file 'ps'
146
+ return nil unless file
147
+
148
+ headers = file.raw.first.split(' ', 11)
149
+ list = file.raw[1..].each.map do |row|
150
+ row.split(' ', 11).each_with_index.each_with_object({}) do |(v, i), obj|
151
+ obj[headers[i]] = v
152
+ end
153
+ end
154
+ { headers: headers, list: list }
155
+ end
156
+
157
+ def systemctl_unit_files
158
+ file = find_file 'systemctl_unit_files'
159
+ return nil unless file
160
+
161
+ all = file.raw[1..-2].map do |x|
162
+ unit, status = x.split
163
+ { unit: unit, status: status }
164
+ end
165
+
166
+ all.reject! { |x| x[:unit].nil? }
167
+ all.sort_by(&:unit)
168
+ end
169
+ end
170
+
171
+ # # Slim Wrapper
172
+ # class List
173
+ # attr_accessor :hosts
174
+
175
+ # def initialize
176
+ # self.hosts = []
177
+ # end
178
+
179
+ # def slim(file, host = nil)
180
+ # Slim::Template.new("#{__dir__}/views/#{file}.slim").render(host)
181
+ # end
182
+ # end