ecl 1.0.0
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 +7 -0
- data/.gitignore +11 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +20 -0
- data/Rakefile +1 -0
- data/bin/console +10 -0
- data/bin/setup +7 -0
- data/eclair.gemspec +29 -0
- data/exe/ecl +6 -0
- data/lib/eclair.rb +22 -0
- data/lib/eclair/cell.rb +101 -0
- data/lib/eclair/color.rb +19 -0
- data/lib/eclair/column.rb +52 -0
- data/lib/eclair/config.rb +68 -0
- data/lib/eclair/console.rb +56 -0
- data/lib/eclair/grid.rb +355 -0
- data/lib/eclair/group.rb +55 -0
- data/lib/eclair/helpers/aws_helper.rb +157 -0
- data/lib/eclair/helpers/benchmark_helper.rb +19 -0
- data/lib/eclair/helpers/common_helper.rb +24 -0
- data/lib/eclair/instance.rb +148 -0
- data/lib/eclair/less_viewer.rb +23 -0
- data/lib/eclair/matcher.rb +19 -0
- data/lib/eclair/version.rb +3 -0
- data/templates/eclrc.template +61 -0
- metadata +155 -0
data/lib/eclair/grid.rb
ADDED
@@ -0,0 +1,355 @@
|
|
1
|
+
module Eclair
|
2
|
+
module Grid
|
3
|
+
include CommonHelper
|
4
|
+
extend self
|
5
|
+
|
6
|
+
HEADER_ROWS = 4
|
7
|
+
SORT_FUNCTIONS = {
|
8
|
+
"Name" => lambda {|i| [i.name.downcase, -i.launch_time.to_i]},
|
9
|
+
}
|
10
|
+
|
11
|
+
def maxy
|
12
|
+
stdscr.maxy - HEADER_ROWS
|
13
|
+
end
|
14
|
+
|
15
|
+
def render_header
|
16
|
+
if mode == :search
|
17
|
+
if cursor
|
18
|
+
header = ["Searching #{@search_str}", "Found #{cursor.name}", ""]
|
19
|
+
else
|
20
|
+
header = ["Searching #{@search_str}", "None Found", ""]
|
21
|
+
end
|
22
|
+
else
|
23
|
+
header = cursor.header
|
24
|
+
end
|
25
|
+
|
26
|
+
header.each_with_index do |line,i|
|
27
|
+
setpos(i,0)
|
28
|
+
clrtoeol
|
29
|
+
addstr(line)
|
30
|
+
end
|
31
|
+
render_help
|
32
|
+
end
|
33
|
+
|
34
|
+
def render_help
|
35
|
+
setpos(3,0)
|
36
|
+
clrtoeol
|
37
|
+
|
38
|
+
helps = {
|
39
|
+
"Enter" => "SSH",
|
40
|
+
"Space" => "Select",
|
41
|
+
"[a-z0-9]" => "Search",
|
42
|
+
"?" => "Inspect",
|
43
|
+
"!" => "Open Ruby REPL",
|
44
|
+
}
|
45
|
+
|
46
|
+
attron(Color.fetch(*config.help_color)) do
|
47
|
+
addstr helps.map{ |key, action|
|
48
|
+
" #{key} => #{action}"
|
49
|
+
}.join(" ").slice(0,stdscr.maxx).ljust(stdscr.maxx)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def cell_width
|
54
|
+
stdscr.maxx/column_count
|
55
|
+
end
|
56
|
+
|
57
|
+
def start
|
58
|
+
assign
|
59
|
+
move_cursor(x: 0, y: 0)
|
60
|
+
render_all
|
61
|
+
end
|
62
|
+
|
63
|
+
def assign
|
64
|
+
sort_function = lambda {|i| [i.name.downcase, -i.launch_time.to_i]}
|
65
|
+
@group_map ||= {}
|
66
|
+
if config.group_by
|
67
|
+
Aws.instances.group_by(&config.group_by).each do |group, instances|
|
68
|
+
if @group_map[group]
|
69
|
+
group_cell = @group_map[group]
|
70
|
+
else
|
71
|
+
col = columns[target_col]
|
72
|
+
group_cell = Group.new(group, col)
|
73
|
+
col << group_cell
|
74
|
+
@group_map[group] = group_cell
|
75
|
+
end
|
76
|
+
instances.each do |i|
|
77
|
+
unless group_cell.find{|j| j.instance_id == i.instance_id}
|
78
|
+
obj = Instance.new(i.instance_id, col)
|
79
|
+
group_cell << obj
|
80
|
+
end
|
81
|
+
end
|
82
|
+
group_cell.items.sort_by!(&sort_function)
|
83
|
+
end
|
84
|
+
else
|
85
|
+
col_limit = (Aws.instances.count - 1) / config.columns + 1
|
86
|
+
iter = Aws.instances.map{|i| Instance.new(i.instance_id)}.sort_by(&sort_function).each
|
87
|
+
columns.each do |col|
|
88
|
+
col_limit.times do
|
89
|
+
begin
|
90
|
+
i = iter.next
|
91
|
+
i.column = col
|
92
|
+
col << i
|
93
|
+
rescue StopIteration
|
94
|
+
break
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def render_all
|
102
|
+
clear
|
103
|
+
columns.each do |cols|
|
104
|
+
cols.each do |c|
|
105
|
+
c.render
|
106
|
+
end
|
107
|
+
end
|
108
|
+
render_header
|
109
|
+
refresh
|
110
|
+
end
|
111
|
+
|
112
|
+
def ssh
|
113
|
+
targets = selected.select{|i| i.is_a?(Instance) && i.connectable?}
|
114
|
+
return if targets.empty?
|
115
|
+
close_screen
|
116
|
+
|
117
|
+
cmd = ""
|
118
|
+
if targets.count == 1
|
119
|
+
target = targets.first
|
120
|
+
cmd = target.ssh_cmd
|
121
|
+
else
|
122
|
+
cmds = []
|
123
|
+
session_name = nil
|
124
|
+
session_cmd = nil
|
125
|
+
|
126
|
+
targets.each_with_index do |target, i|
|
127
|
+
if i==0
|
128
|
+
if ENV['TMUX']
|
129
|
+
cmds << "tmux new-window -- '#{target.ssh_cmd}'"
|
130
|
+
else
|
131
|
+
session_name = "eclair#{Time.now.to_i}"
|
132
|
+
session_cmd = "-t #{session_name}"
|
133
|
+
cmds << "tmux new-session -d -s #{session_name} -- '#{target.ssh_cmd}'"
|
134
|
+
end
|
135
|
+
else
|
136
|
+
cmds << "tmux split-window #{session_cmd} -- '#{target.ssh_cmd}'"
|
137
|
+
cmds << "tmux select-layout #{session_cmd} tiled"
|
138
|
+
end
|
139
|
+
end
|
140
|
+
cmds << "tmux set-window-option #{session_cmd} synchronize-panes on"
|
141
|
+
cmds << "tmux attach #{session_cmd}" unless ENV['TMUX']
|
142
|
+
cmd = cmds.join(" && ")
|
143
|
+
end
|
144
|
+
system cmd
|
145
|
+
exit
|
146
|
+
end
|
147
|
+
|
148
|
+
def column_count
|
149
|
+
columns.count{|col| !col.empty?}
|
150
|
+
end
|
151
|
+
|
152
|
+
def columns
|
153
|
+
@columns ||= config.columns.times.map{|idx| Column.new(idx)}.to_a
|
154
|
+
end
|
155
|
+
|
156
|
+
def rows
|
157
|
+
columns.map(&:count).max
|
158
|
+
end
|
159
|
+
|
160
|
+
def target_col
|
161
|
+
counts = columns.map(&:count)
|
162
|
+
counts.index(counts.min)
|
163
|
+
end
|
164
|
+
|
165
|
+
def selected
|
166
|
+
@selected ||= []
|
167
|
+
end
|
168
|
+
|
169
|
+
def mode
|
170
|
+
@mode ||= :navi
|
171
|
+
end
|
172
|
+
|
173
|
+
def select
|
174
|
+
end_search if mode == :search
|
175
|
+
if mode == :navi
|
176
|
+
@mode = :select
|
177
|
+
cursor.toggle_select
|
178
|
+
end
|
179
|
+
cursor.toggle_select
|
180
|
+
if selected.empty?
|
181
|
+
@mode = :navi
|
182
|
+
cursor.toggle_select
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
def move key
|
187
|
+
end_search if mode == :search
|
188
|
+
mx,my = {
|
189
|
+
KEY_UP => [0,-1],
|
190
|
+
"k" => [0,-1],
|
191
|
+
KEY_DOWN => [0,1],
|
192
|
+
"j" => [0,1],
|
193
|
+
KEY_LEFT => [-1,0],
|
194
|
+
"h" => [-1,0],
|
195
|
+
KEY_RIGHT => [1,0],
|
196
|
+
"l" => [1,0],
|
197
|
+
}[key]
|
198
|
+
|
199
|
+
newx = (@x + mx) % column_count
|
200
|
+
newy = (@y + my - columns[@x].scroll + columns[newx].scroll)
|
201
|
+
if my != 0
|
202
|
+
newy %= columns[newx].count
|
203
|
+
end
|
204
|
+
if newy >= columns[newx].count
|
205
|
+
newy = columns[newx].count-1
|
206
|
+
end
|
207
|
+
|
208
|
+
move_cursor(x: newx, y: newy)
|
209
|
+
end
|
210
|
+
|
211
|
+
def cursor
|
212
|
+
@x ||= -1
|
213
|
+
@y ||= -1
|
214
|
+
if @x >=0 && @y >= 0
|
215
|
+
columns[@x][@y]
|
216
|
+
else
|
217
|
+
nil
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
def query
|
222
|
+
return nil if @search_str == ""
|
223
|
+
result = columns.map(&:expand).flatten.grep(Instance).map(&:name).max_by{|name| name.score @search_str}
|
224
|
+
return nil if result.score(@search_str) == 0.0
|
225
|
+
result
|
226
|
+
end
|
227
|
+
|
228
|
+
def start_search
|
229
|
+
@rollback_cursor = [@x, @y]
|
230
|
+
@rollback_mode = @mode
|
231
|
+
@search_str = ""
|
232
|
+
end
|
233
|
+
|
234
|
+
def end_search
|
235
|
+
if cursor
|
236
|
+
move_cursor(mode: @rollback_mode)
|
237
|
+
@mode = @rollback_mode
|
238
|
+
else
|
239
|
+
cancel_search
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
def cancel_search
|
244
|
+
x,y = @rollback_cursor
|
245
|
+
move_cursor(x: x, y: y, mode: @rollback_mode)
|
246
|
+
end
|
247
|
+
|
248
|
+
|
249
|
+
def move_cursor **options, &block
|
250
|
+
if cursor
|
251
|
+
cursor.toggle_select if mode == :navi
|
252
|
+
cursor.decurrent
|
253
|
+
end
|
254
|
+
|
255
|
+
new_mode = options.delete(:mode)
|
256
|
+
if new_mode && mode != new_mode
|
257
|
+
case new_mode
|
258
|
+
when :search
|
259
|
+
start_search
|
260
|
+
end
|
261
|
+
@mode = new_mode
|
262
|
+
end
|
263
|
+
|
264
|
+
if block
|
265
|
+
@x, @y = block.call
|
266
|
+
else
|
267
|
+
@x = options.delete(:x) || @x
|
268
|
+
@y = options.delete(:y) || @y
|
269
|
+
end
|
270
|
+
|
271
|
+
if cursor
|
272
|
+
cursor.toggle_select if mode == :navi
|
273
|
+
cursor.current
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
def search key
|
278
|
+
@search_str ||= ""
|
279
|
+
|
280
|
+
move_cursor(mode: :search) do
|
281
|
+
if key
|
282
|
+
@search_str = @search_str+key
|
283
|
+
else
|
284
|
+
@search_str.chop!
|
285
|
+
end
|
286
|
+
|
287
|
+
goto = query
|
288
|
+
|
289
|
+
result = nil
|
290
|
+
columns.each do |col|
|
291
|
+
target = col.find {|item| item.is_a?(Instance) && item.name == goto}
|
292
|
+
if target
|
293
|
+
result = [target.x, target.y]
|
294
|
+
break
|
295
|
+
end
|
296
|
+
end
|
297
|
+
|
298
|
+
result || [-1,-1]
|
299
|
+
end
|
300
|
+
|
301
|
+
render_header
|
302
|
+
end
|
303
|
+
|
304
|
+
def resize
|
305
|
+
columns.each{|col| col.scroll = 0}
|
306
|
+
render_all
|
307
|
+
cursor.check_scroll
|
308
|
+
end
|
309
|
+
|
310
|
+
def cursor_inspect
|
311
|
+
close_screen
|
312
|
+
LessViewer.show cursor.info
|
313
|
+
render_all
|
314
|
+
end
|
315
|
+
|
316
|
+
def debug
|
317
|
+
trap("INT") { raise Interrupt }
|
318
|
+
close_screen
|
319
|
+
binding.pry
|
320
|
+
render_all
|
321
|
+
trap("INT") { exit }
|
322
|
+
end
|
323
|
+
|
324
|
+
def next_sort_function
|
325
|
+
@sort_function_idx ||= -1
|
326
|
+
@sort_function_idx = (@sort_function_idx + 1) % SORT_FUNCTIONS.count
|
327
|
+
SORT_FUNCTIONS.values[@sort_function_idx]
|
328
|
+
end
|
329
|
+
|
330
|
+
def sorted_by
|
331
|
+
SORT_FUNCTIONS.keys[@sort_function_idx]
|
332
|
+
end
|
333
|
+
|
334
|
+
def change_sort
|
335
|
+
stored_cursor = cursor
|
336
|
+
sort_function = next_sort_function
|
337
|
+
columns.each do |column|
|
338
|
+
column.groups.each do |group|
|
339
|
+
group.items.sort_by!(&sort_function)
|
340
|
+
end
|
341
|
+
end
|
342
|
+
@x, @y = stored_cursor.x, stored_cursor.y
|
343
|
+
render_all
|
344
|
+
end
|
345
|
+
|
346
|
+
def reload
|
347
|
+
clear
|
348
|
+
addstr("reloading")
|
349
|
+
refresh
|
350
|
+
Aws.reload_instances
|
351
|
+
assign
|
352
|
+
render_all
|
353
|
+
end
|
354
|
+
end
|
355
|
+
end
|
data/lib/eclair/group.rb
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
module Eclair
|
2
|
+
class Group < Cell
|
3
|
+
array_accessor :items
|
4
|
+
|
5
|
+
def initialize group_name, column = nil
|
6
|
+
super
|
7
|
+
@group_name = group_name
|
8
|
+
@items = []
|
9
|
+
@column = column
|
10
|
+
end
|
11
|
+
|
12
|
+
def << instance
|
13
|
+
@items << instance
|
14
|
+
end
|
15
|
+
|
16
|
+
def x
|
17
|
+
column.x
|
18
|
+
end
|
19
|
+
|
20
|
+
def y
|
21
|
+
column.index(self)
|
22
|
+
end
|
23
|
+
|
24
|
+
def color
|
25
|
+
super(*config.group_color)
|
26
|
+
end
|
27
|
+
|
28
|
+
def format
|
29
|
+
" #{@group_name} (#{count(&:connectable?)}) #{select_indicator}"
|
30
|
+
end
|
31
|
+
|
32
|
+
def header
|
33
|
+
["Group #{@group_name}",
|
34
|
+
"#{count} Instances Total",
|
35
|
+
"#{count(&:running?)} Instances Running"]
|
36
|
+
end
|
37
|
+
|
38
|
+
def items
|
39
|
+
@items
|
40
|
+
end
|
41
|
+
|
42
|
+
def name
|
43
|
+
@group_name
|
44
|
+
end
|
45
|
+
|
46
|
+
def object
|
47
|
+
@group_name
|
48
|
+
end
|
49
|
+
|
50
|
+
def info
|
51
|
+
@items.map(&:info)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
@@ -0,0 +1,157 @@
|
|
1
|
+
module Eclair
|
2
|
+
module Aws
|
3
|
+
extend self
|
4
|
+
|
5
|
+
def ec2
|
6
|
+
@ec2 ||= ::Aws::EC2::Client.new
|
7
|
+
end
|
8
|
+
|
9
|
+
def route53
|
10
|
+
@route53 ||= ::Aws::Route53::Client.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def instances
|
14
|
+
fetch_all unless @instances
|
15
|
+
@instances
|
16
|
+
end
|
17
|
+
|
18
|
+
def instance_map
|
19
|
+
@instance_map
|
20
|
+
end
|
21
|
+
|
22
|
+
def images **options
|
23
|
+
if options.delete :force
|
24
|
+
@images_thread.join
|
25
|
+
end
|
26
|
+
@images || []
|
27
|
+
end
|
28
|
+
|
29
|
+
def images?
|
30
|
+
!@images_thread.alive?
|
31
|
+
end
|
32
|
+
|
33
|
+
def dns_records **options
|
34
|
+
if options.delete :force
|
35
|
+
@route53_thread.join
|
36
|
+
end
|
37
|
+
@dns_records || []
|
38
|
+
end
|
39
|
+
|
40
|
+
def dns_records?
|
41
|
+
!@route53_thread.alive?
|
42
|
+
end
|
43
|
+
|
44
|
+
def security_groups **options
|
45
|
+
if options.delete :force
|
46
|
+
@security_groups_thread.join
|
47
|
+
end
|
48
|
+
@security_groups || []
|
49
|
+
end
|
50
|
+
|
51
|
+
def security_groups?
|
52
|
+
!@security_groups_thread.alive?
|
53
|
+
end
|
54
|
+
|
55
|
+
def reload_instances
|
56
|
+
return if @reload_thread && @reload_thread.alive?
|
57
|
+
|
58
|
+
if @reload_thread
|
59
|
+
@instances = @r_instances
|
60
|
+
@instance_map = @r_instances_map
|
61
|
+
@images += @new_images
|
62
|
+
@dns_records = @r_dns_records
|
63
|
+
@security_groups = @r_security_groups
|
64
|
+
Grid.assign
|
65
|
+
@reload_thread = nil
|
66
|
+
end
|
67
|
+
|
68
|
+
return if @last_reloaded && Time.now - @last_reloaded < 5
|
69
|
+
|
70
|
+
@reload_thread = Thread.new do
|
71
|
+
r_instances, r_instances_map = fetch_instances
|
72
|
+
@new_instances = r_instances.map(&:instance_id) - @instances.map(&:instance_id)
|
73
|
+
if new_instances.empty?
|
74
|
+
@new_images = []
|
75
|
+
else
|
76
|
+
image_ids = @new_instances.map(&:image_id)
|
77
|
+
[
|
78
|
+
Thread.new do
|
79
|
+
@new_images = fetch_images(image_ids)
|
80
|
+
end,
|
81
|
+
|
82
|
+
Thread.new do
|
83
|
+
@r_security_groups = fetch_security_groups
|
84
|
+
end
|
85
|
+
].each(&:join)
|
86
|
+
end
|
87
|
+
@last_reloaded = Time.now
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
def fetch_images image_ids
|
94
|
+
ec2.describe_images(image_ids: image_ids).images.flatten
|
95
|
+
end
|
96
|
+
|
97
|
+
def fetch_dns_records
|
98
|
+
hosted_zone_ids = route53.list_hosted_zones.hosted_zones.map(&:id)
|
99
|
+
hosted_zone_ids.map { |hosted_zone_id|
|
100
|
+
route53.list_resource_record_sets(hosted_zone_id: hosted_zone_id).map { |resp|
|
101
|
+
resp.resource_record_sets
|
102
|
+
}
|
103
|
+
}.flatten
|
104
|
+
end
|
105
|
+
|
106
|
+
def fetch_security_groups
|
107
|
+
ec2.describe_security_groups.map{ |resp|
|
108
|
+
resp.security_groups
|
109
|
+
}.flatten
|
110
|
+
end
|
111
|
+
|
112
|
+
def fetch_instances
|
113
|
+
instance_map = {}
|
114
|
+
|
115
|
+
instances = ec2.describe_instances.map{ |resp|
|
116
|
+
resp.data.reservations.map(&:instances)
|
117
|
+
}.flatten
|
118
|
+
|
119
|
+
instances.each do |i|
|
120
|
+
instance_map[i.instance_id] = i
|
121
|
+
end
|
122
|
+
|
123
|
+
[instances, instance_map]
|
124
|
+
end
|
125
|
+
|
126
|
+
|
127
|
+
def fetch_all
|
128
|
+
@instances, @instance_map = fetch_instances
|
129
|
+
|
130
|
+
image_ids = @instances.map(&:image_id)
|
131
|
+
|
132
|
+
if @threads
|
133
|
+
@threads.each{ |t| t.kill }
|
134
|
+
end
|
135
|
+
|
136
|
+
Thread.abort_on_exception = true
|
137
|
+
|
138
|
+
@threads = []
|
139
|
+
|
140
|
+
@threads << @images_thread = Thread.new do
|
141
|
+
@images = fetch_images(image_ids)
|
142
|
+
end
|
143
|
+
|
144
|
+
@threads << @route53_thread = Thread.new do
|
145
|
+
@dns_records = fetch_dns_records
|
146
|
+
end
|
147
|
+
|
148
|
+
@threads << @security_groups_thread = Thread.new do
|
149
|
+
@security_groups = fetch_security_groups
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def find_username image_id
|
154
|
+
config.ssh_username.call(image_id, @images[image_id].name)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|