ecl 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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