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 +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
|