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 +3 -0
- data/lib/cluster_bomb/actest.rb +63 -0
- data/lib/cluster_bomb/bomb.rb +256 -0
- data/lib/cluster_bomb/bomb_shell.rb +325 -0
- data/lib/cluster_bomb/cli.rb +82 -0
- data/lib/cluster_bomb/cluster.rb +298 -0
- data/lib/cluster_bomb/configuration.rb +83 -0
- data/lib/cluster_bomb/dispatcher.rb +110 -0
- data/lib/cluster_bomb/history.rb +42 -0
- data/lib/cluster_bomb/logging.rb +35 -0
- data/lib/cluster_bomb/roles.rb +56 -0
- data/lib/cluster_bomb/shawties.rb +46 -0
- data/lib/cluster_bomb/stdtasks.rb +16 -0
- metadata +122 -0
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'cluster_bomb/bomb'
|
2
|
+
require 'cluster_bomb/logging'
|
3
|
+
require 'getoptlong'
|
4
|
+
class Cli
|
5
|
+
include ClusterBomb::Bomb
|
6
|
+
def initialize
|
7
|
+
@execution_environment={}
|
8
|
+
super
|
9
|
+
self.load File.join(File.dirname(__FILE__),'stdtasks.rb')
|
10
|
+
if File.exists? 'Bombfile'
|
11
|
+
self.load 'Bombfile'
|
12
|
+
else
|
13
|
+
puts "WARNING: no Bombfile in current directory"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
def process_args
|
17
|
+
if ARGV.length < 1
|
18
|
+
puts "syntax: cb <task> [options]"
|
19
|
+
puts "available tasks: "
|
20
|
+
@tasks.each do |n,t|
|
21
|
+
puts " #{t.name} - #{t.description}"
|
22
|
+
end
|
23
|
+
exit
|
24
|
+
end
|
25
|
+
@task=ARGV[0].to_sym
|
26
|
+
|
27
|
+
# Get program opts
|
28
|
+
opts = GetoptLong.new(
|
29
|
+
[ '--logfile', '-l', GetoptLong::REQUIRED_ARGUMENT ],
|
30
|
+
[ '--logmode', GetoptLong::REQUIRED_ARGUMENT ],
|
31
|
+
[ '--user', GetoptLong::REQUIRED_ARGUMENT ],
|
32
|
+
[ '--nolog', GetoptLong::NO_ARGUMENT ]
|
33
|
+
)
|
34
|
+
logfile=nil
|
35
|
+
logmode=nil
|
36
|
+
nolog=false
|
37
|
+
@user=nil
|
38
|
+
opts.each do |opt, arg|
|
39
|
+
case opt
|
40
|
+
when '--logfile'
|
41
|
+
logfile=arg
|
42
|
+
when '--logmode'
|
43
|
+
raise "valid log modes are a or w" unless ['a','w'].include? arg
|
44
|
+
logmode=arg
|
45
|
+
when '--nolog'
|
46
|
+
nolog=true
|
47
|
+
when '--user'
|
48
|
+
@user=arg
|
49
|
+
end
|
50
|
+
end
|
51
|
+
if logfile && !logmode
|
52
|
+
logmode='a'
|
53
|
+
end
|
54
|
+
unless nolog
|
55
|
+
ClusterBomb::Logging.log_init
|
56
|
+
ClusterBomb::Logging.log_enable(logfile,logmode)
|
57
|
+
end
|
58
|
+
# Set up environment, with special attention to roles
|
59
|
+
ARGV[1..-1].each do |arg|
|
60
|
+
pair = arg.split('=')
|
61
|
+
if pair.length == 2
|
62
|
+
k = pair[0].strip.to_sym
|
63
|
+
if k == :roles
|
64
|
+
list=pair[1].split(',').collect{|r|r.strip.to_sym}
|
65
|
+
@execution_environment[:roles]=list
|
66
|
+
elsif k == :hosts
|
67
|
+
list=pair[1].split(',').collect{|r|r.strip}
|
68
|
+
@execution_environment[:hosts]=list
|
69
|
+
else
|
70
|
+
self.set(k,pair[1].strip)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
def main
|
76
|
+
process_args
|
77
|
+
switch_user(@user) if @user
|
78
|
+
exec @task, {:roles=>@execution_environment[:roles], :hosts=>@execution_environment[:hosts]}
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
Cli.new.main
|
@@ -0,0 +1,298 @@
|
|
1
|
+
require 'net/ssh'
|
2
|
+
require 'net/scp'
|
3
|
+
require 'net/sftp'
|
4
|
+
require 'cluster_bomb/logging'
|
5
|
+
|
6
|
+
# Represents a cluster of hosts over which to operate
|
7
|
+
module ClusterBomb
|
8
|
+
class Cluster
|
9
|
+
include Logging
|
10
|
+
class Host
|
11
|
+
attr_accessor :name
|
12
|
+
attr_accessor :buffer_stderr, :buffer_stdout, :buffer_console
|
13
|
+
attr_accessor :exception
|
14
|
+
attr_accessor :data
|
15
|
+
attr_accessor :connected
|
16
|
+
attr_accessor :connect_failed
|
17
|
+
|
18
|
+
def initialize(name, cluster)
|
19
|
+
self.name=name
|
20
|
+
self.clear!
|
21
|
+
self.data={}
|
22
|
+
self.connected=false
|
23
|
+
self.connect_failed=false
|
24
|
+
@cluster = cluster
|
25
|
+
end
|
26
|
+
def nickname
|
27
|
+
@cluster.nicknames[self.name]
|
28
|
+
end
|
29
|
+
def stdout
|
30
|
+
buffer_stdout.join('')
|
31
|
+
end
|
32
|
+
def stderr
|
33
|
+
buffer_stderr.join('')
|
34
|
+
end
|
35
|
+
def console
|
36
|
+
buffer_console.join('')
|
37
|
+
end
|
38
|
+
def clear!
|
39
|
+
self.buffer_stderr=[]
|
40
|
+
self.buffer_stdout=[]
|
41
|
+
self.buffer_console=[]
|
42
|
+
self.exception=nil
|
43
|
+
end
|
44
|
+
# match stdout, returning arrays
|
45
|
+
def match(rex, default=nil)
|
46
|
+
m = rex.match self.stdout
|
47
|
+
if m
|
48
|
+
if m.length > 2
|
49
|
+
m[1..-1]
|
50
|
+
else
|
51
|
+
m[1]
|
52
|
+
end
|
53
|
+
else
|
54
|
+
default
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
attr_accessor :hosts, :nicknames
|
60
|
+
def initialize(user, options={})
|
61
|
+
@user_name ||= user
|
62
|
+
@connections=[]
|
63
|
+
@hosts=[]
|
64
|
+
@ssh_options=options
|
65
|
+
@connection_mutex = Mutex.new
|
66
|
+
@connected=false
|
67
|
+
@connection_cache={}
|
68
|
+
@nicknames={}
|
69
|
+
@start_time =nil
|
70
|
+
@max_time=nil
|
71
|
+
end
|
72
|
+
|
73
|
+
def connect!(host_list)
|
74
|
+
return if host_list.empty?
|
75
|
+
@hosts=[]
|
76
|
+
# Build up results rray
|
77
|
+
host_list.each {|hostname| @hosts << Host.new(hostname, self)}
|
78
|
+
|
79
|
+
# Connect. Build up connections array
|
80
|
+
# Seems like there would be an async call to do this -- but it looks like
|
81
|
+
# Not -- so we resort to threads
|
82
|
+
puts "Connecting to #{hosts.length} hosts"
|
83
|
+
ensure_connected!
|
84
|
+
@connected=true
|
85
|
+
puts "Connected to #{hosts.length} hosts"
|
86
|
+
end
|
87
|
+
|
88
|
+
# Credentials to be used for next connection attempt.
|
89
|
+
def credentials(user, ssh_opts)
|
90
|
+
@ssh_options=ssh_opts
|
91
|
+
@user_name = user
|
92
|
+
end
|
93
|
+
|
94
|
+
# Be sure all hosts are connected that have not previously failed to connect
|
95
|
+
def ensure_connected!
|
96
|
+
if @ssh_options[:timeout]
|
97
|
+
total_timeout = @ssh_options[:timeout] * 2
|
98
|
+
else
|
99
|
+
total_timeout = 30
|
100
|
+
end
|
101
|
+
# puts "Total timeout: #{total_timeout}"
|
102
|
+
@connections=[]
|
103
|
+
hosts_to_connect = @hosts.inject(0) {|sum,h| sum += (h.connect_failed ? 0:1)}
|
104
|
+
# puts "#{hosts_to_connect} to connect"
|
105
|
+
@hosts.each do |host|
|
106
|
+
if @connection_cache[host.name] || host.connected
|
107
|
+
@connection_mutex.synchronize { @connections << {:connection=>@connection_cache[host.name], :host=>host} }
|
108
|
+
host.connected=true
|
109
|
+
elsif !host.connect_failed
|
110
|
+
Thread.new {
|
111
|
+
begin
|
112
|
+
#puts "Connecting #{host.name}"
|
113
|
+
c = Net::SSH.start(host.name, @user_name, @ssh_options)
|
114
|
+
@connection_cache[host.name] = c
|
115
|
+
@connection_mutex.synchronize { @connections << {:connection=>c, :host=>host} }
|
116
|
+
host.connected=true
|
117
|
+
#puts "Connected #{host.name}"
|
118
|
+
rescue Exception => e
|
119
|
+
host.connect_failed = true
|
120
|
+
host.connected=false
|
121
|
+
error "Unable to connect to #{host.name}\n#{e.message}"
|
122
|
+
@connection_mutex.synchronize {@connections << {:connection=>nil, :host=>host} }
|
123
|
+
host.exception=e
|
124
|
+
end
|
125
|
+
}
|
126
|
+
end
|
127
|
+
end
|
128
|
+
s = Time.now
|
129
|
+
loop do
|
130
|
+
l=0
|
131
|
+
@connection_mutex.synchronize { l = @connections.length }
|
132
|
+
break if l == hosts_to_connect
|
133
|
+
sleep(0.1)
|
134
|
+
if Time.now - s > total_timeout
|
135
|
+
puts "Warning -- total connection time expired"
|
136
|
+
puts "Failed to connect:"
|
137
|
+
hosts.each do |h|
|
138
|
+
unless h.connected
|
139
|
+
puts " #{h.name}"
|
140
|
+
h.connect_failed=true
|
141
|
+
# TODO: Need to handle this situations much better. Attempt to kill thread and/or mark connection in cache as unreachable
|
142
|
+
end
|
143
|
+
end
|
144
|
+
break
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def set_run_timer(options)
|
150
|
+
if options[:max_run_time]
|
151
|
+
@max_time = options[:max_run_time].to_i
|
152
|
+
@start_time = Time.now
|
153
|
+
else
|
154
|
+
@max_time = nil
|
155
|
+
@start_time = nil
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
def download(remote, local, options={}, &task)
|
160
|
+
ensure_connected!
|
161
|
+
set_run_timer(options)
|
162
|
+
@connections.each do |c|
|
163
|
+
next if c[:connection].nil?
|
164
|
+
c[:completed]=false
|
165
|
+
c[:connection].scp.download(remote,local)
|
166
|
+
end
|
167
|
+
event_loop(task)
|
168
|
+
@hosts
|
169
|
+
end
|
170
|
+
|
171
|
+
def upload(local, remote, options={}, &task)
|
172
|
+
opts={:chunk_size=>16384}.merge(options)
|
173
|
+
ensure_connected!
|
174
|
+
set_run_timer(options)
|
175
|
+
@connections.each do |c|
|
176
|
+
next if c[:connection].nil?
|
177
|
+
c[:completed]=false
|
178
|
+
c[:connection].scp.upload(local,remote, opts)
|
179
|
+
end
|
180
|
+
event_loop(task)
|
181
|
+
@hosts
|
182
|
+
end
|
183
|
+
|
184
|
+
# Build sudo-fied command. Really only works for bash afaik
|
185
|
+
def mksudo(command)
|
186
|
+
"sudo sh -c '(#{command})'"
|
187
|
+
end
|
188
|
+
|
189
|
+
def run(command, options={}, &task)
|
190
|
+
# Execute
|
191
|
+
ensure_connected!
|
192
|
+
if options[:sudo]
|
193
|
+
command=mksudo(command)
|
194
|
+
end
|
195
|
+
# puts "Command: #{command}"
|
196
|
+
set_run_timer(options)
|
197
|
+
@connections.each do |c|
|
198
|
+
next if c[:connection].nil?
|
199
|
+
c[:completed]=false
|
200
|
+
c[:host].clear! if options[:echo] # Clear out before starting
|
201
|
+
c[:connection].exec command do |ch, stream, data|
|
202
|
+
c[:host].buffer_console << data
|
203
|
+
if stream == :stderr
|
204
|
+
c[:host].buffer_stderr << data
|
205
|
+
else
|
206
|
+
"#{c[:host].name}=> #{data}"
|
207
|
+
c[:host].buffer_stdout << data
|
208
|
+
end
|
209
|
+
puts "#{c[:host].name}::#{data}" if options[:debug]
|
210
|
+
print "." if options[:dotty]
|
211
|
+
end
|
212
|
+
end
|
213
|
+
event_loop(task, options)
|
214
|
+
@hosts
|
215
|
+
end
|
216
|
+
|
217
|
+
def event_loop(task, options={})
|
218
|
+
# Event loop
|
219
|
+
condition = Proc.new { |s| s.busy?(true) }
|
220
|
+
# Count up non-nil connections
|
221
|
+
count = 0
|
222
|
+
@connections.each {|c| count +=1 if c[:connection]}
|
223
|
+
loop do
|
224
|
+
@connections.each do |conn|
|
225
|
+
next if conn[:connection].nil? || conn[:completed]
|
226
|
+
ex=nil
|
227
|
+
busy=true
|
228
|
+
begin
|
229
|
+
busy = conn[:connection].process(0.1, &condition)
|
230
|
+
if @start_time && Time.now - @start_time > @max_time
|
231
|
+
# Soft exception here -- stay connected
|
232
|
+
conn[:host].exception = Exception.new("Execution time exceeded: #{@max_time}")
|
233
|
+
puts "Execution time exceeded: #{@max_time}"
|
234
|
+
busy=false
|
235
|
+
end
|
236
|
+
rescue Exception => e
|
237
|
+
# As far as I can tell, if we ever get here, the session is fucked.
|
238
|
+
# Close out the connection and indicate that we want to be reconnected later
|
239
|
+
# In general, its upload/download exceptions that get us here. Even bad filenames can do the trick
|
240
|
+
puts "#{e.message}"
|
241
|
+
host = conn[:host]
|
242
|
+
@connection_cache[host.name].close
|
243
|
+
@connection_cache[host.name]=nil
|
244
|
+
host.connected=false # disconnect
|
245
|
+
busy=false
|
246
|
+
end
|
247
|
+
if !busy
|
248
|
+
conn[:completed] = true
|
249
|
+
count -=1
|
250
|
+
h = conn[:host]
|
251
|
+
if task
|
252
|
+
task.call(h)
|
253
|
+
elsif options[:echo]
|
254
|
+
puts "#{h.name}\n#{h.console}\n"
|
255
|
+
end
|
256
|
+
end
|
257
|
+
end
|
258
|
+
break if count <=0
|
259
|
+
end
|
260
|
+
# Reset these
|
261
|
+
@start_time=nil
|
262
|
+
@max_time = nil
|
263
|
+
end
|
264
|
+
|
265
|
+
def connected?
|
266
|
+
@connected
|
267
|
+
end
|
268
|
+
|
269
|
+
def disconnect!
|
270
|
+
if @connections
|
271
|
+
@connection_cache.each do |k, conn|
|
272
|
+
begin
|
273
|
+
conn.close
|
274
|
+
rescue Exception=>e
|
275
|
+
puts "Non-fatal EXCEPTION closing connection: #{e.message}"
|
276
|
+
end
|
277
|
+
end
|
278
|
+
end
|
279
|
+
@hosts.each {|h| h.connected=false}
|
280
|
+
@connections=[]
|
281
|
+
@connection_cache={}
|
282
|
+
@connected=false
|
283
|
+
@hosts=[]
|
284
|
+
end
|
285
|
+
|
286
|
+
def reset!
|
287
|
+
@connections=[]
|
288
|
+
@connected=false
|
289
|
+
end
|
290
|
+
|
291
|
+
def clear!
|
292
|
+
@hosts.each {|h| h.clear!}
|
293
|
+
end
|
294
|
+
def error(msg)
|
295
|
+
puts msg
|
296
|
+
end
|
297
|
+
end
|
298
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
module ClusterBomb
|
3
|
+
module Configuration
|
4
|
+
HOME=File.join(ENV["HOME"], ".cluster_bomb")
|
5
|
+
CFG_FILE_NAME = File.join(HOME,"config.yaml")
|
6
|
+
KEYS={
|
7
|
+
:screen_width=>{:default=>120},
|
8
|
+
:logging=>{:default=>true},
|
9
|
+
:logfile=>{:default=>"logs/cb-#{Time.now().strftime('%m-%d-%Y')}.log}"},
|
10
|
+
:max_run_time=>{:default=>nil},
|
11
|
+
:max_history=>{:default=>1000},
|
12
|
+
:username=>{:default=>ENV["USER"]}
|
13
|
+
}
|
14
|
+
def initialize
|
15
|
+
@configuration={}
|
16
|
+
@ssh_options_for_user={} # By user
|
17
|
+
Configuration.check_dir
|
18
|
+
end
|
19
|
+
# Check for home dir, and create it if it doesn't exist
|
20
|
+
def self.check_dir
|
21
|
+
`mkdir #{HOME}` unless File.exists? HOME
|
22
|
+
end
|
23
|
+
# Save it out
|
24
|
+
def save
|
25
|
+
Configuration.check_dir
|
26
|
+
File.open(CFG_FILE_NAME,"w") {|f| f.write(@configuration.to_yaml)}
|
27
|
+
end
|
28
|
+
def configuration
|
29
|
+
@configuration
|
30
|
+
end
|
31
|
+
def set(key, val)
|
32
|
+
k = key.class==Symbol ? key : key.to_sym
|
33
|
+
# TODO: VALIDATION
|
34
|
+
raise "Invalid setting [#{key}]" unless KEYS[k]
|
35
|
+
@configuration[k] = val
|
36
|
+
end
|
37
|
+
def get(key)
|
38
|
+
k = key.class==Symbol ? key : key.to_sym
|
39
|
+
raise "Config.get ==> Uknown key #{k}" unless (@configuration.has_key? k or KEYS.has_key? k)
|
40
|
+
ret = @configuration[k] || KEYS[k][:default]
|
41
|
+
ret
|
42
|
+
end
|
43
|
+
def keys
|
44
|
+
KEYS.keys
|
45
|
+
end
|
46
|
+
def valid_key?(k)
|
47
|
+
KEYS.has_key? k.to_sym
|
48
|
+
end
|
49
|
+
def load!
|
50
|
+
Configuration.check_dir
|
51
|
+
begin
|
52
|
+
buf = File.read(CFG_FILE_NAME)
|
53
|
+
rescue
|
54
|
+
`touch #{CFG_FILE_NAME}`
|
55
|
+
buf = File.read(CFG_FILE_NAME)
|
56
|
+
end
|
57
|
+
unless buf.empty?
|
58
|
+
@configuration = YAML.load(buf)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
def ssh_options(username)
|
62
|
+
if has_ssh_options? username
|
63
|
+
if !@ssh_options_for_user[username]
|
64
|
+
@ssh_options_for_user[username] = YAML.load(File.read(File.join(HOME,"ssh_#{username}.yml")))
|
65
|
+
end
|
66
|
+
end
|
67
|
+
@ssh_options_for_user[username] || {}
|
68
|
+
end
|
69
|
+
def has_ssh_options?(username)
|
70
|
+
return true if @ssh_options_for_user[username]
|
71
|
+
File.exists? File.join(HOME,"ssh_#{username}.yml")
|
72
|
+
end
|
73
|
+
def method_missing(symbol, *args)
|
74
|
+
str = symbol.to_s
|
75
|
+
if str.match(/=$/)
|
76
|
+
self.set(str.sub('=', ''),args[0])
|
77
|
+
else
|
78
|
+
self.get(str)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
@@ -0,0 +1,110 @@
|
|
1
|
+
require 'readline'
|
2
|
+
module ClusterBomb
|
3
|
+
module Dispatcher
|
4
|
+
COMMANDS = [
|
5
|
+
{:rex=> /^qu.*/, :method=>:quit, :name=>':quit', :description=>'Quit. Ctrl-d also works'},
|
6
|
+
{:rex=> /^wi.*/, :method=>:with, :name=>':with', :description=>'Set roles in use. This will determine the remote host set. ex: :with apache,database'},
|
7
|
+
{:rex=> /^use.*/, :method=>:use, :name=>':use', :description=>'Run a command against a host or hosts. Ex. use foo.bar.com,x.y.com ls -la'},
|
8
|
+
{:rex=> /^ex.*/, :method=>:exec, :name=>':exec', :description=>'Execute a configured task task'},
|
9
|
+
{:rex=> /^li.*/, :method=>:list, :name=>':list', :description=>'List available tasks to run'},
|
10
|
+
{:rex=> /^his.*/, :method=>:history, :name=>':history', :description=>'Command history. Can be followed by a filter regexp'},
|
11
|
+
{:rex=> /^disc.*/, :method=>:disconnect, :name=>':disconnect', :description=>'Disconnect all cached connections.'},
|
12
|
+
{:rex=> /^up.*/, :method=>:upload, :name=>':upload', :description=>'Upload one or more files to servers. Supports wildcards and autocomplete for source filename. ex: upload sourcepath destpath'},
|
13
|
+
{:rex=> /^he.*/, :method=>:help, :name=>':help', :description=>'Quick help.'},
|
14
|
+
{:rex=> /^se.*/, :method=>:set, :name=>':set', :description=>'Set a variable'},
|
15
|
+
{:rex=> /^ho.*/, :method=>:host_list, :name=>':hosts', :description=>'List current hosts'},
|
16
|
+
{:rex=> /^switch/, :method=>:switch, :name=>':switch', :description=>'Switch user'},
|
17
|
+
{:rex=> /^sudo/, :method=>:sudo, :name=>':sudo', :description=>'Sudo mode on/off'}
|
18
|
+
]
|
19
|
+
COMMAND_AUTOCOMPLETE = COMMANDS.collect{|c|c[:name]} + ['with','use']
|
20
|
+
def init_autocomplete
|
21
|
+
Readline.completion_proc=proc {|s| self.dispatcher_completion_proc(s)}
|
22
|
+
Readline.completer_word_break_characters = 7.chr
|
23
|
+
Readline.completion_case_fold = true
|
24
|
+
Readline.completion_append_character = ''
|
25
|
+
end
|
26
|
+
def process_cmd(line)
|
27
|
+
# cmd = cmd.strip.downcase
|
28
|
+
m=line.match(/([^ ]+) *?(.*)/)
|
29
|
+
return true unless m
|
30
|
+
cmd = m[1]
|
31
|
+
params = m[2] || ""
|
32
|
+
params.strip!
|
33
|
+
return false if cmd =~ /^q.*/
|
34
|
+
found=false
|
35
|
+
COMMANDS.each do |cr|
|
36
|
+
if cmd =~ cr[:rex]
|
37
|
+
# p = cmd_param(cmd)
|
38
|
+
self.send cr[:method], params
|
39
|
+
found=true
|
40
|
+
break
|
41
|
+
end
|
42
|
+
end
|
43
|
+
if !found
|
44
|
+
puts "Available commands:"
|
45
|
+
COMMANDS.each do |cr|
|
46
|
+
puts " #{cr[:name]} - #{cr[:description]}"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
return true
|
50
|
+
end
|
51
|
+
def dispatcher_completion_proc(s)
|
52
|
+
# No line buffer for mac, so no way to get command context
|
53
|
+
# Very lame with libedit. Will not return candidates, and
|
54
|
+
# We cannot get the current line so we can do context-based
|
55
|
+
# edits
|
56
|
+
ret=[]
|
57
|
+
tokens = s.split(' ')
|
58
|
+
if s =~ /^:?\w.* $/
|
59
|
+
tokens << ''
|
60
|
+
elsif s =~ /^\\\w*/
|
61
|
+
tokens << ''
|
62
|
+
end
|
63
|
+
# Initial command only
|
64
|
+
if tokens.length <= 1 && tokens[0] != '\\'
|
65
|
+
ret = COMMAND_AUTOCOMPLETE.grep(/^#{s}/)
|
66
|
+
else
|
67
|
+
if tokens[0]=~/:?with.*/
|
68
|
+
ret = secondary_completion_proc(tokens, @bomb.role_list.collect{|r| r[:name].to_s})
|
69
|
+
elsif tokens[0]=~/:?use.*/ && @bomb.valid_role?(:all)
|
70
|
+
ret = secondary_completion_proc(tokens, @bomb.servers([:all]))
|
71
|
+
elsif tokens[0]=~/:exec.*/
|
72
|
+
ret = secondary_completion_proc(tokens, @bomb.task_list.collect{|t| t.name.to_s})
|
73
|
+
elsif tokens[0]=~/:upload.*/
|
74
|
+
ret = dir_completion_proc(tokens)
|
75
|
+
elsif tokens[0]=~/:set.*/
|
76
|
+
ret = secondary_completion_proc(tokens, @bomb.configuration.keys)
|
77
|
+
elsif tokens[0]=~/^\\/
|
78
|
+
ret = shawtie_names.grep(/#{tokens[0][1..-1]}/)
|
79
|
+
ret = ret.collect {|r|"\\#{r}"}
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
ret
|
84
|
+
end
|
85
|
+
|
86
|
+
def dir_completion_proc(tokens)
|
87
|
+
choices=dir_list(tokens[1])
|
88
|
+
secondary_completion_proc(tokens,choices)
|
89
|
+
end
|
90
|
+
|
91
|
+
def dir_list(token)
|
92
|
+
m = token.match(/(.*\/).*$/)
|
93
|
+
if m && m[1]
|
94
|
+
ret=Dir.glob("#{m[1]}*")
|
95
|
+
else
|
96
|
+
ret=Dir.glob("*")
|
97
|
+
end
|
98
|
+
ret.collect {|p| (File.directory? p) ? "#{p}/" : "#{p}"}
|
99
|
+
end
|
100
|
+
|
101
|
+
def secondary_completion_proc(tokens, choices)
|
102
|
+
if tokens[1]==''
|
103
|
+
choices
|
104
|
+
else
|
105
|
+
tokens[1] = tokens[1].gsub(/\./,'\.')
|
106
|
+
choices.grep(/^#{tokens[1]}/).collect {|c| "#{tokens[0]} #{c}"}
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end # dispatcher
|
110
|
+
end # clusterbomb
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'readline'
|
2
|
+
module ClusterBomb
|
3
|
+
module History
|
4
|
+
HISTORY_FILE=File.join(Configuration::HOME,'history')
|
5
|
+
|
6
|
+
def save_history
|
7
|
+
save_history = Readline::HISTORY.to_a.dup
|
8
|
+
start=0
|
9
|
+
start = save_history.length - @max_history if save_history.length > @max_history
|
10
|
+
|
11
|
+
File.open(HISTORY_FILE, "w") do |f|
|
12
|
+
save_history[start..-1].each {|l| f.puts(l)}
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def load_history(max_history=500)
|
17
|
+
@max_history = max_history
|
18
|
+
return unless File.exists? HISTORY_FILE
|
19
|
+
File.open(HISTORY_FILE, "r") do |f|
|
20
|
+
c=0
|
21
|
+
while line = f.gets
|
22
|
+
Readline::HISTORY.push(line.strip)
|
23
|
+
if c==0 && libedit? # libedit work-around
|
24
|
+
Readline::HISTORY.push(line.strip)
|
25
|
+
end
|
26
|
+
c+=1
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
# Cheesy check to see if libedit is in use -- will affect history
|
31
|
+
def libedit?
|
32
|
+
libedit = false
|
33
|
+
# If NotImplemented then this might be libedit
|
34
|
+
begin
|
35
|
+
Readline.emacs_editing_mode
|
36
|
+
rescue NotImplementedError
|
37
|
+
libedit = true
|
38
|
+
end
|
39
|
+
libedit
|
40
|
+
end
|
41
|
+
end # History
|
42
|
+
end # ClusterBomb
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module ClusterBomb
|
2
|
+
module Logging
|
3
|
+
DEFAULT_LOGFILENAME="logs/cb-#{Time.now().strftime('%m-%d-%Y')}.log"
|
4
|
+
def self.log_init
|
5
|
+
@logging_enabled=false
|
6
|
+
@io=nil
|
7
|
+
end
|
8
|
+
def self.log_enable(filename=DEFAULT_LOGFILENAME, filemode='a')
|
9
|
+
filename ||= DEFAULT_LOGFILENAME
|
10
|
+
filemode ||= 'a'
|
11
|
+
log_dir = File.dirname(filename)
|
12
|
+
`mkdir -p #{log_dir}` unless File.exists? log_dir
|
13
|
+
@io = File.open(filename,filemode)
|
14
|
+
@logging_enabled=true
|
15
|
+
end
|
16
|
+
def self.log_disable
|
17
|
+
@io.close if @io
|
18
|
+
@io=nil
|
19
|
+
@logging_enabled=false
|
20
|
+
end
|
21
|
+
def self.log(msg)
|
22
|
+
if @logging_enabled && @io
|
23
|
+
@io.puts "#{Time.now().strftime('%m-%d-%Y %H:%M:%S')} - #{msg}"
|
24
|
+
@io.flush
|
25
|
+
end
|
26
|
+
end
|
27
|
+
def self.puts(msg)
|
28
|
+
Kernel.puts msg
|
29
|
+
self.log(msg)
|
30
|
+
end
|
31
|
+
def puts(msg)
|
32
|
+
Logging.puts(msg)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end # ClusterBomb
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module Roles
|
2
|
+
def role(*args, &task)
|
3
|
+
if args.length==2
|
4
|
+
name=args[0]
|
5
|
+
servers=args[1]
|
6
|
+
ra=servers.split(',')
|
7
|
+
elsif args.length==1 && task != nil
|
8
|
+
name=args[0]
|
9
|
+
ra = yield(task)
|
10
|
+
else
|
11
|
+
raise "role command takes a role name and a list of hosts OR a block yielding a list of hosts"
|
12
|
+
end
|
13
|
+
set_role(name, ra)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Clear out role
|
17
|
+
def clear_role(name)
|
18
|
+
rl = @roles[name.to_sym]
|
19
|
+
@roles[name.to_sym] =[] if rl
|
20
|
+
end
|
21
|
+
|
22
|
+
# produce a list of servers from role list
|
23
|
+
# empty or nil role list implies all roles
|
24
|
+
def servers(role_list)
|
25
|
+
@roles ||={}
|
26
|
+
ra=[]
|
27
|
+
if role_list && !role_list.empty?
|
28
|
+
if role_list.class==String
|
29
|
+
ra = role_list.split(',').collect{|r| r.to_sym}
|
30
|
+
else
|
31
|
+
ra=role_list
|
32
|
+
end
|
33
|
+
end
|
34
|
+
server_list=[]
|
35
|
+
ra.each do |role|
|
36
|
+
raise "Role #{role} not found" unless @roles[role]
|
37
|
+
server_list += @roles[role]
|
38
|
+
end
|
39
|
+
server_list
|
40
|
+
end
|
41
|
+
|
42
|
+
def valid_role?(name)
|
43
|
+
@roles[name.to_sym]
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
def set_role(name, hosts)
|
48
|
+
@roles ||={}
|
49
|
+
@roles[name] ||=[]
|
50
|
+
# puts "ROLE: #{name} #{hosts.inspect}"
|
51
|
+
hosts.each do |host|
|
52
|
+
host.strip!
|
53
|
+
@roles[name] << host unless @roles[name].include? host
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|