cluster_bomb 0.2.6

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