cluster_bomb 0.2.6

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.
data/bin/cb ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/ruby
2
+ # Remove me if gem
3
+ load 'cluster_bomb/cli.rb'
@@ -0,0 +1,63 @@
1
+ require 'readline'
2
+
3
+ class Test
4
+ @stty_save = `stty -g`.chomp
5
+ def run()
6
+ Readline.completion_proc=proc {|s| self.main_completion_proc(s)}
7
+ Readline.completer_word_break_characters = 7.chr
8
+ Readline.completion_case_fold = true
9
+ Readline.completion_append_character = ''
10
+ while(true) do
11
+ line = read_line
12
+ break if !line
13
+ end
14
+ end
15
+ def read_line
16
+ begin
17
+ line = Readline.readline("> ", true)
18
+ if line =~ /^\s*$/ || Readline::HISTORY.to_a[-2] == line
19
+ Readline::HISTORY.pop
20
+ end
21
+ rescue Interrupt => e
22
+ system('stty', @stty_save)
23
+ return nil
24
+ end
25
+ return line if line.nil? # Ctrl-d
26
+ line.strip!
27
+ return line
28
+ end
29
+
30
+ def main_completion_proc(s)
31
+ # No line buffer for mac, so no way to get command context
32
+ # Very lame with libedit. Will not return candidates, and
33
+ # We cannot get the current line so we can do context-based
34
+ # edits
35
+ cmds = ['foo','bar','plushy','food','bear','plum','ls','los','lsu']
36
+ ret=[]
37
+ tokens = s.split(' ')
38
+ if s =~ /^\w.* $/
39
+ tokens << ''
40
+ end
41
+ # Initial command only
42
+ if tokens.length <= 1
43
+ ret = cmds.grep(/^#{s}/)
44
+ else
45
+ if tokens[0]=='ls'
46
+ ret = foo_completion_proc(tokens)
47
+ end
48
+ end
49
+ ret
50
+ end
51
+ def foo_completion_proc(tokens)
52
+ files=Dir.glob('*')
53
+ if tokens[1]==''
54
+ candidates=files
55
+ candidates
56
+ else
57
+ candidates = files.grep(/^#{tokens[1]}/)
58
+ candidates.collect {|c| "#{tokens[0]} #{c}"}
59
+ end
60
+ end
61
+ end
62
+
63
+ Test.new.run
@@ -0,0 +1,256 @@
1
+ require 'cluster_bomb/cluster.rb'
2
+ require 'cluster_bomb/roles.rb'
3
+ require 'cluster_bomb/configuration.rb'
4
+ require 'cluster_bomb/logging.rb'
5
+
6
+ # TODO: persistent configuration
7
+ # TODO: logging
8
+ module ClusterBomb
9
+ # Task Runner module
10
+ # Loads task files and runs tasks
11
+ # All tasks run within the context of the class implementing this module
12
+ module Bomb
13
+ include Roles
14
+ include Logging
15
+ class Task
16
+ attr_accessor :proc, :roles, :description, :sudo
17
+ attr_reader :group, :name, :filename
18
+ attr_reader :options
19
+ def initialize(name, group, filename=nil, filetime=nil, opts={})
20
+ @name=name
21
+ self.roles=[]
22
+ @filename=filename
23
+ @group = group
24
+ @filetime=filetime
25
+ @options=opts
26
+ @sudo = opts[:sudo]
27
+ end
28
+ def updated?
29
+ return false if !self.filename
30
+ File.stat(self.filename).mtime != @filetime
31
+ end
32
+ end
33
+
34
+ class Config
35
+ include Configuration
36
+ end
37
+
38
+ attr_accessor :env, :auto_reload, :configuration,:interactive, :username, :sudo_mode
39
+ def initialize
40
+ @sudo_mode = false
41
+ @tasks||={}
42
+ @cluster||=nil
43
+ self.env={}
44
+ @reloading=false
45
+ @current_load_file=nil
46
+ @current_load_file_time=nil
47
+ self.auto_reload=true
48
+ @configuration = Config.new
49
+ @configuration.load!
50
+ @username = @configuration.username
51
+ raise "Unable to get a default user name. Exiting..." unless @username
52
+ @interactive=false
53
+ super
54
+ end
55
+
56
+ def interactive?
57
+ @interactive
58
+ end
59
+
60
+ def group(str)
61
+ @current_group=str
62
+ end
63
+
64
+ def desc(str)
65
+ @current_desription=str
66
+ end
67
+
68
+ def task(name, options={}, &task)
69
+ t = Task.new(name, @current_group, @current_load_file,@current_load_file_time, options )
70
+ raise "task #{t.name} is already defined" if @tasks[t.name] && !@reloading
71
+ @tasks[t.name]=t
72
+ t.proc = task
73
+ t.roles=options[:roles]
74
+ t.description=@current_desription
75
+
76
+ @current_description=''
77
+ end
78
+ def role_list
79
+ ret=[]
80
+ @roles.each do |k,v|
81
+ ret << {:name=>k, :hostnames=>v}
82
+ end
83
+ ret
84
+ end
85
+ def load_str(str)
86
+ begin
87
+ self.instance_eval(str)
88
+ rescue Exception => e
89
+ puts "Exception while loading: #{@current_load_file}"
90
+ raise e
91
+ end
92
+ ssh_options = @configuration.ssh_options(username)
93
+ @cluster = Cluster.new(username,ssh_options) unless @cluster
94
+ @current_load_file=nil
95
+ @current_load_file_time=nil
96
+ end
97
+
98
+ def reload(fn)
99
+ @reloading=true
100
+ load(fn)
101
+ @reloading=false
102
+ end
103
+
104
+ def load(fn)
105
+ @current_group=fn.split('/').last
106
+ s = File.read(fn)
107
+ @current_load_file=fn
108
+ @current_load_file_time=File.stat(fn).mtime
109
+ self.load_str(s)
110
+ end
111
+
112
+ def set(name, value=nil)
113
+ self.env[name.to_sym]=value
114
+ code=<<-EODEF
115
+ def #{name}
116
+ self.env[:#{name}]
117
+ end
118
+ def #{name}=(rhs)
119
+ self.env[:#{name}]=rhs
120
+ end
121
+ EODEF
122
+ self.instance_eval(code)
123
+ end
124
+
125
+ def ensure_var(name, value=nil)
126
+ return if env.has_key? name.to_sym
127
+ set(name, value)
128
+ end
129
+
130
+ def exists?(attrname)
131
+ env.has_key? attrname.to_sym
132
+ end
133
+
134
+ def clear_env!
135
+ env.each_key do |k|
136
+ self.instance_eval("undef #{k.to_s}; undef #{k.to_s}=")
137
+ end
138
+ self.env={}
139
+ end
140
+
141
+ def switch_user(user)
142
+ if @configuration.has_ssh_options? user
143
+ ssh_options = @configuration.ssh_options(user)
144
+ else
145
+ ssh_options={}
146
+ end
147
+ @username = ssh_options[:user] || user
148
+ @cluster.credentials(@username, ssh_options)
149
+ @cluster.disconnect!
150
+ end
151
+
152
+ def exec(name, options={})
153
+ sudo_save = @sudo_mode
154
+ # @cluster.reset!
155
+ server_list=server_list_from_options(options)
156
+ t = @tasks[name]
157
+ raise "TASK NOT FOUND: #{name}" unless t
158
+ @sudo_mode = true if t.sudo # Turn it on if sudo is true
159
+ raise "Task not found: #{name}" if t.nil?
160
+ if self.auto_reload && t.updated?
161
+ puts "Reloading #{t.filename}"
162
+ reload(t.filename)
163
+ t = @tasks[name]
164
+ end
165
+ raise "Task not found: #{name}" if t.nil?
166
+ server_list = self.servers(t.roles) if server_list.empty?
167
+ # puts "CONNECTED: #{@cluster.connected?}"
168
+ @cluster.connect!(server_list) unless @cluster.connected? && (!options[:roles] && !options[:hosts])
169
+ raise "Task #{name} not found" unless t
170
+ t.proc.call
171
+ @sudo_mode = sudo_save
172
+ end
173
+
174
+ def download(remote, local, options={}, &task)
175
+ @cluster.download(remote, local, options={}, &task)
176
+ end
177
+
178
+ def upload(local, remote, options={}, &task)
179
+ files=[]
180
+
181
+ server_list=[]
182
+ if options[:roles]
183
+ server_list = self.servers(options[:roles])
184
+ elsif options[:hosts]
185
+ server_list=options[:hosts]
186
+ end
187
+ if !server_list.empty?
188
+ @cluster.reset!
189
+ @cluster.connect!(server_list)
190
+ end
191
+
192
+ if local.index('*')
193
+ files=Dir.glob(local)
194
+ else
195
+ files << local
196
+ end
197
+ files.each do |f|
198
+ next if File.stat(f).directory?
199
+ puts "Uploading: #{f}"
200
+ @cluster.upload(f, remote, options, &task)
201
+ end
202
+ end
203
+
204
+ def run(command, options={}, &task)
205
+ server_list=server_list_from_options(options)
206
+ if !server_list.empty?
207
+ @cluster.reset!
208
+ @cluster.connect!(server_list)
209
+ end
210
+ options[:sudo] = @sudo_mode unless options.has_key? :sudo
211
+ # use max_run_time environment variable if not passed in by caller
212
+ options[:max_run_time] = self.configuration.max_run_time unless options[:max_run_time]
213
+ @cluster.run(command, options, &task)
214
+ end
215
+
216
+ def server_list_from_options(options)
217
+ server_list=[]
218
+ if options[:roles]
219
+ server_list = self.servers(options[:roles])
220
+ elsif options[:hosts]
221
+ server_list=options[:hosts]
222
+ end
223
+ server_list
224
+ end
225
+
226
+ # primarily for shell use to change roles
227
+ def reconnect!(roles)
228
+ @cluster.reset!
229
+ @cluster.connect!(self.servers(roles))
230
+ end
231
+
232
+ def valid_task?(name)
233
+ @tasks[name] ? true : false
234
+ end
235
+
236
+ def get_task(name)
237
+ @tasks[name]
238
+ end
239
+
240
+ def disconnect!
241
+ @cluster.disconnect!
242
+ end
243
+
244
+ def task_list
245
+ ret=[]
246
+ @tasks.each{|k,v| ret << v }
247
+ ret
248
+ end
249
+ def clear
250
+ @cluster.clear!
251
+ end
252
+ def hosts
253
+ @cluster.hosts
254
+ end
255
+ end
256
+ end
@@ -0,0 +1,325 @@
1
+ require 'readline'
2
+ require 'cluster_bomb/history.rb'
3
+ require 'cluster_bomb/logging.rb'
4
+ require 'cluster_bomb/dispatcher.rb'
5
+ require 'cluster_bomb/shawties.rb'
6
+ # TODO: upgrade command history (persist, no repeats)
7
+ # TODO: settings
8
+ module ClusterBomb
9
+ class BombShell
10
+ include History
11
+ include Dispatcher
12
+ include Shawties
13
+ WELCOME="Welcome to the BombShell v 0.2.1, Cowboy\n:help -- quick help"
14
+ def initialize(bomb)
15
+ @bomb=bomb
16
+ @bomb.interactive=true
17
+ @stty_save = `stty -g`.chomp
18
+ # Default settings
19
+ @roles=[]
20
+ end
21
+
22
+ def loop
23
+ puts WELCOME
24
+ load_history @bomb.configuration.max_history
25
+ init_autocomplete
26
+ load_shawties!
27
+ while true
28
+ cmd = read_line
29
+ break if cmd.nil?
30
+ next if cmd.empty?
31
+ # See if we're repezating a command
32
+ if m = cmd.match(/^:(\d+)$/)
33
+ cmd = Readline::HISTORY[m[1].to_i]
34
+ next if cmd.nil?
35
+ puts cmd
36
+ Readline::HISTORY.pop
37
+ Readline::HISTORY.push(cmd)
38
+ end
39
+ Logging.log(cmd)
40
+ break if !process_input(cmd)
41
+ end
42
+ save_history
43
+ puts "Exiting..."
44
+ Logging.log_disable
45
+ end
46
+
47
+ def process_input(buf, reprocess=false)
48
+ if buf.index(':')==0
49
+ return false if !process_cmd(buf[1..-1])
50
+ elsif buf.index('!')==0
51
+ self.shell(buf)
52
+ elsif buf.index('\\')==0 && !reprocess
53
+ self.shawtie(buf)
54
+ else
55
+ begin
56
+ run(buf)
57
+ rescue Exception => e
58
+ puts "Exception on run command: #{e.message}"
59
+ puts e.backtrace
60
+ end
61
+ end
62
+ return true
63
+ end
64
+
65
+ def shawtie(cmd)
66
+ if cmd.index(/^\\d /) == 0
67
+ m = cmd.match(/^\\d +(\w+) +(\d+)/)
68
+ if m
69
+ sdef = Readline::HISTORY[m[2].to_i]
70
+ if sdef
71
+ puts "Defined short #{m[1]} :: #{sdef}"
72
+ define_shawtie(m[1],sdef)
73
+ end
74
+ else
75
+ m = cmd.match(/^\\d +(\w+) +(.+)$/)
76
+ if m.nil?
77
+ m = cmd.match(/^\\d +(\w+)/)
78
+ define_shawtie(m[1],nil)
79
+ puts "Undefed short #{m[1]}"
80
+ else
81
+ define_shawtie(m[1],m[2])
82
+ puts "Defined short #{m[1]} - [#{m[2]}]"
83
+ end
84
+ end
85
+ elsif cmd.index(/^\\l$/) == 0
86
+ shawties_list
87
+ else
88
+ m=cmd.match(/\\(\w+)/)
89
+ if !m
90
+ shawties_list
91
+ else
92
+ sdef = get_shawtie(m[1])
93
+ if sdef
94
+ puts "Run short: #{m[1]} :: #{sdef}"
95
+ self.process_input(sdef)
96
+ end
97
+ end
98
+ end
99
+ end
100
+
101
+ def read_line
102
+ total_hosts = @bomb.hosts.length
103
+ total_connected_hosts = 0
104
+ @bomb.hosts.each {|h| total_connected_hosts +=1 if h.connected}
105
+ begin
106
+ sm = @bomb.sudo_mode ? '[SUDO] ' : ''
107
+ line = Readline.readline("#{sm}(#{@bomb.username}) #{total_connected_hosts}/#{total_hosts}> ", true)
108
+ if line =~ /^\s*$/ || Readline::HISTORY.to_a[-2] == line
109
+ Readline::HISTORY.pop
110
+ end
111
+ rescue Interrupt => e
112
+ system('stty', @stty_save)
113
+ return nil
114
+ end
115
+ return line if line.nil? # Ctrl-d
116
+ line.strip!
117
+ return line
118
+ end
119
+
120
+ def shell(cmd)
121
+ cmdline=cmd[1..-1].strip
122
+ puts `#{cmdline}`
123
+ end
124
+
125
+ def disconnect(p)
126
+ @bomb.disconnect!
127
+ end
128
+
129
+ def history(p)
130
+ Readline::HISTORY.to_a.each_with_index do |h,i|
131
+ if p.nil?
132
+ puts " #{i}: #{h}"
133
+ else
134
+ puts " #{i}: #{h}" if h.match(p)
135
+ end
136
+ end
137
+ end
138
+ def set(p)
139
+ return if p.nil? || p=='shell'
140
+ parts = p.split('=')
141
+ if parts.length > 2
142
+ puts "syntax: set <name>=<value"
143
+ end
144
+ k = parts[0].strip.to_sym
145
+ unless @bomb.configuration.valid_key? k
146
+ puts "Unknown configuration setting #{k}"
147
+ return
148
+ end
149
+ if parts.length == 2
150
+ @bomb.configuration.set(k,parts[1].strip)
151
+ else
152
+ @bomb.configuration.set(k,nil)
153
+ end
154
+ end
155
+ def list(p)
156
+ tg = {}
157
+ @bomb.task_list.each do |t|
158
+ tg[t.group] ||=[]
159
+ tg[t.group] << t
160
+ end
161
+ puts "Available tasks (usually autocompletable):"
162
+ tg.to_a.sort{|a,b|a[0]<=>b[0] }.each do |ta|
163
+ puts " #{ta[0]}"
164
+ t_sorted = ta[1].sort {|a,b| a.name.to_s <=> b.name.to_s}
165
+ t_sorted.each do |t|
166
+ puts " [#{t.name}] - #{t.description}"
167
+ end
168
+ end
169
+ end
170
+ def exec(p)
171
+ return if p=='shell'
172
+ task_name = p.split(' ').first unless p.nil?
173
+ if task_name.nil? || !@bomb.valid_task?(task_name.to_sym)
174
+ puts "Task missing or not found"
175
+ self.list(nil)
176
+ return
177
+ end
178
+ # @bomb.clear_env!
179
+ roles=@roles
180
+ opts = @bomb.get_task(task_name.to_sym).options || {}
181
+ roles = opts[:roles] if opts[:roles] && opts[:sticky_roles]
182
+ self.process_task_args(p)
183
+ puts "NOTE Using sticky roles defined on task: #{roles.inspect}" if opts[:sticky_roles]
184
+ @bomb.clear
185
+ begin
186
+ unless roles.empty?
187
+ @bomb.exec(task_name.to_sym, {:roles=>roles})
188
+ else
189
+ @bomb.exec(task_name.to_sym)
190
+ end
191
+ rescue Exception=>e
192
+ puts "ERROR: #{e.message}"
193
+ # p e.backtrace
194
+ end
195
+ # Cheesy -- clear out enviroment variables passed in with task to prevent accidental reuse
196
+ @bomb.clear_env!
197
+ end
198
+
199
+ def process_task_args(p)
200
+ arg_keys=[]
201
+ args=p.split(' ')
202
+ return [] if args.length <=1
203
+ args[1..-1].each do |kv|
204
+ pair = kv.split('=')
205
+ if pair.length == 2
206
+ k = pair[0].strip.to_sym
207
+ arg_keys << k
208
+ @bomb.set(k,pair[1].strip)
209
+ end
210
+ end
211
+ arg_keys # Return these so we can clear them when the task is done
212
+ end
213
+
214
+ def use(p)
215
+ unless p.nil?
216
+ match = p.strip.match(/(.*?) +(.*)/)
217
+ if match
218
+ host_list = match[1]
219
+ cmd=match[2].strip
220
+ else
221
+ host_list = p
222
+ end
223
+ hosts=host_list.split(',').collect{|r|r.strip}
224
+ else
225
+ puts("Host list argument required")
226
+ return
227
+ end
228
+ @roles=[] # Nil this out so future commands hit this host
229
+ begin
230
+ self.run(cmd, hosts)
231
+ rescue Exception => e
232
+ puts "ERROR: #{e.message}"
233
+ end
234
+ end
235
+
236
+ def with(p)
237
+ unless p.nil?
238
+ match = p.strip.match(/(.*?) +(.*)/)
239
+ if match
240
+ role_list = match[1]
241
+ cmd=match[2].strip
242
+ else
243
+ role_list = p
244
+ end
245
+ roles=role_list.split(',').collect{|r|r.strip.to_sym}
246
+ bad_role=roles.detect{|r| !@bomb.valid_role? r }
247
+ end
248
+ if p.nil? || bad_role
249
+ p.nil? ? puts("Role argument required") : puts("Unknown role: #{bad_role}")
250
+ puts "Available roles:"
251
+ @bomb.role_list.each do |r|
252
+ puts " #{r[:name]} (#{r[:hostnames].length} hosts)"
253
+ end
254
+ return
255
+ end
256
+ @roles=roles
257
+ begin
258
+ @bomb.reconnect!(@roles)
259
+ run cmd if cmd
260
+ rescue Exception => e
261
+ puts "ERROR: #{e.message}"
262
+ end
263
+ end
264
+
265
+ def upload(p)
266
+ source, dest = p.split(' ') unless p.nil?
267
+ if source.nil? || dest.nil? || p.nil?
268
+ puts "syntax: upload sourcepath destpath (no wildcards)"
269
+ return true
270
+ end
271
+ begin
272
+ @bomb.upload(source, dest)
273
+ rescue Exception => e
274
+ puts "ERROR: #{e.message}"
275
+ end
276
+ end
277
+
278
+ def help(p=nil)
279
+ puts "Anything entered at the shell prompt will be executed on the current set of remote servers. "
280
+ puts "Anything preceded by a : will be interpreted as a cluster_bomb command"
281
+ puts "Available cluster_bomb commands:"
282
+ Dispatcher::COMMANDS.each do |cr|
283
+ puts " #{cr[:name]} - #{cr[:description]}"
284
+ end
285
+ puts ":<nnn> will re-execute a command from the history"
286
+ puts "Use the bang (!) to execute something in the local shell. ex !ls -la"
287
+ end
288
+
289
+ def host_list(p)
290
+ Logging.puts "Roles in use: #{@roles.join(',')}"
291
+ hl = @bomb.hosts.collect {|h| h.name}
292
+ Logging.puts "#{hl.length} active hosts"
293
+ Logging.puts "#{hl.join(',')}"
294
+ end
295
+
296
+ def switch(p)
297
+ return if p.empty?
298
+ @bomb.switch_user(p)
299
+ end
300
+ def sudo(p)
301
+ @bomb.sudo_mode = !@bomb.sudo_mode
302
+ end
303
+ def run(cmd, host_list=nil)
304
+ @bomb.clear
305
+ opts={}
306
+ opts[:hosts]=host_list if host_list
307
+ opts[:sudo] = true if @sudo_mode
308
+ @bomb.run(cmd, opts) do |r|
309
+ if r.exception
310
+ puts "#{r.name} => EXCEPTION: #{r.exception.message}"
311
+ else
312
+ output = r.console
313
+ ll = output.length + r.name.length + 3
314
+ if ll > @bomb.configuration.screen_width.to_i || output.index('\n')
315
+ Logging.puts "=== #{r.name} ==="
316
+ Logging.puts output
317
+ else
318
+ Logging.puts "#{r.name} => #{output}"
319
+ end
320
+ end
321
+ end
322
+ end
323
+ end # Bombshell
324
+ end # Module
325
+