cluster_bomb 0.2.6

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