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.
@@ -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