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.
@@ -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