ant_hill 0.3.1
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/.gitignore +4 -0
- data/Gemfile +4 -0
- data/Rakefile +1 -0
- data/ant_hill.gemspec +29 -0
- data/bin/spawn_queen +14 -0
- data/lib/ant_hill.rb +36 -0
- data/lib/ant_hill/ant.rb +126 -0
- data/lib/ant_hill/ant_colony.rb +212 -0
- data/lib/ant_hill/configuration.rb +154 -0
- data/lib/ant_hill/connection_pool.rb +98 -0
- data/lib/ant_hill/connections/ssh_connection.rb +67 -0
- data/lib/ant_hill/creep.rb +301 -0
- data/lib/ant_hill/creep_modifier.rb +150 -0
- data/lib/ant_hill/log.rb +35 -0
- data/lib/ant_hill/queen.rb +334 -0
- data/lib/ant_hill/version.rb +4 -0
- data/lib/tasks/ant_hill.rake +48 -0
- data/spec/ant_hill/ant_colony_spec.rb +118 -0
- data/spec/ant_hill/ant_spec.rb +101 -0
- data/spec/ant_hill/configuration_spec.rb +201 -0
- data/spec/ant_hill/creep_modifier_spec.rb +30 -0
- data/spec/ant_hill/creep_spec.rb +165 -0
- data/spec/spec_helper.rb +1 -0
- data/spec/support/config.yml +43 -0
- metadata +117 -0
@@ -0,0 +1,98 @@
|
|
1
|
+
module AntHill
|
2
|
+
# Exception
|
3
|
+
class NoFreeConnectionError < Exception; end
|
4
|
+
# Base class for storing connections
|
5
|
+
class ConnectionPool
|
6
|
+
|
7
|
+
# Attribute readers
|
8
|
+
# +creep+:: +Creep+ object
|
9
|
+
attr_reader :creep
|
10
|
+
include DRbUndumped
|
11
|
+
|
12
|
+
# Initialize
|
13
|
+
# +creep+:: creep for which we'll create connections
|
14
|
+
def initialize(creep)
|
15
|
+
@creep = creep
|
16
|
+
@connection_pool = []
|
17
|
+
end
|
18
|
+
|
19
|
+
# Execute command on creep
|
20
|
+
def exec(command)
|
21
|
+
conn = get_connection
|
22
|
+
if conn
|
23
|
+
begin
|
24
|
+
execute(conn, command)
|
25
|
+
rescue Timeout::Error => e
|
26
|
+
kill_connection(conn)
|
27
|
+
raise e
|
28
|
+
end
|
29
|
+
else
|
30
|
+
logger.error "Couldn't find any free connection or create new one"
|
31
|
+
raise NoFreeConnectionError
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Find free connection
|
36
|
+
def get_connection
|
37
|
+
@connection_pool.delete_if{ |connection| closed?(connection) }
|
38
|
+
connection = @connection_pool.find{|c| !c.busy?}
|
39
|
+
return connection if connection
|
40
|
+
new_conn = nil
|
41
|
+
begin
|
42
|
+
Timeout::timeout( 10 ) do
|
43
|
+
new_conn = get_new
|
44
|
+
end
|
45
|
+
rescue Timeout::Error => e
|
46
|
+
return nil
|
47
|
+
end
|
48
|
+
@connection_pool << new_conn if new_conn
|
49
|
+
new_conn
|
50
|
+
end
|
51
|
+
|
52
|
+
# Close all connections
|
53
|
+
def destroy
|
54
|
+
@connection_pool.each{|connection| kill_connection(connection)}
|
55
|
+
end
|
56
|
+
|
57
|
+
# logger object
|
58
|
+
def logger
|
59
|
+
creep.logger
|
60
|
+
end
|
61
|
+
|
62
|
+
# Check if connection is closed
|
63
|
+
# Should be redefined in child class
|
64
|
+
# +connection+:: connection to check
|
65
|
+
def closed?(connection)
|
66
|
+
raise "Should be implemented in child class"
|
67
|
+
end
|
68
|
+
|
69
|
+
# Check if connection is busy
|
70
|
+
# Should be redefined in child class
|
71
|
+
# +connection+:: connection to check
|
72
|
+
def busy?(connection)
|
73
|
+
raise "Should be implemented in child class"
|
74
|
+
end
|
75
|
+
|
76
|
+
# Kill connection
|
77
|
+
# Should be redefined in child class
|
78
|
+
# +connection+:: connection to kill
|
79
|
+
def kill_connection(connection)
|
80
|
+
raise "Should be implemented in child class"
|
81
|
+
end
|
82
|
+
|
83
|
+
# Establish new connection
|
84
|
+
# Should be redefined in child class
|
85
|
+
def get_new
|
86
|
+
raise "Should be implemented in child class"
|
87
|
+
end
|
88
|
+
|
89
|
+
# Execute command on connection
|
90
|
+
# Should be redefined in child class
|
91
|
+
# +connection+:: connection where execute command
|
92
|
+
# +command+:: cpmmad to execute
|
93
|
+
def execute(connection, command)
|
94
|
+
raise "Should be implemented in child class"
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'net/ssh'
|
2
|
+
module AntHill
|
3
|
+
# Child of ConnectionPool
|
4
|
+
class SSHConnection < ConnectionPool
|
5
|
+
include DRbUndumped
|
6
|
+
|
7
|
+
# Check if SSH connection is closed
|
8
|
+
# +connection+:: SSH connection to check
|
9
|
+
def closed?(connection)
|
10
|
+
return true if connection.closed?
|
11
|
+
begin
|
12
|
+
# Hack to check connection isn't dead
|
13
|
+
connection.exec!('true') unless connection.busy?
|
14
|
+
rescue Net::SSH::Exception, SystemCallError => e
|
15
|
+
return true
|
16
|
+
end
|
17
|
+
return false
|
18
|
+
end
|
19
|
+
|
20
|
+
# Check if SSH connection is busy
|
21
|
+
# +connection+:: SSH connection to check
|
22
|
+
def busy?(connection)
|
23
|
+
connection.busy?
|
24
|
+
end
|
25
|
+
|
26
|
+
# Get new SSH connection
|
27
|
+
def get_new
|
28
|
+
logger.debug "Establishing connection for #{creep.user}@#{creep.host} passwd:#{creep.password}"
|
29
|
+
ssh = Net::SSH.start(creep.host, creep.user, {:password => creep.password, :verbose => (ENV['SSH_DEBUG'] && ENV['SSH_DEBUG'].to_sym) || :fatal })
|
30
|
+
ssh.send_global_request("keepalive@openssh.com")
|
31
|
+
ssh
|
32
|
+
rescue Net::SSH::Exception => ex
|
33
|
+
logger.error "There was an exception in method get_new for SSConnection. Details #{ex}:\n#{ex.backtrace}"
|
34
|
+
return nil
|
35
|
+
rescue SystemCallError => ex
|
36
|
+
logger.error "There was an system error in method get_new for SSConnection. Details #{ex}:\n#{ex.backtrace}"
|
37
|
+
return nil
|
38
|
+
end
|
39
|
+
|
40
|
+
# Execute command on SSH connection
|
41
|
+
# +connection+:: SSH connection
|
42
|
+
# +command+:: command to execute
|
43
|
+
def execute(connection, command)
|
44
|
+
stdout = ""
|
45
|
+
stderr = ""
|
46
|
+
connection.exec!(command) do |ch, stream, data|
|
47
|
+
stderr << data if stream == :stderr
|
48
|
+
stdout << data if stream == :stdout
|
49
|
+
end
|
50
|
+
[stdout, stderr]
|
51
|
+
rescue Net::SSH::Exception => ex
|
52
|
+
logger.error "There was an exception in method execute for SSHConnection. Details #{ex}:\n#{ex.backtrace}"
|
53
|
+
kill_connection(connection)
|
54
|
+
raise NoFreeConnectionError
|
55
|
+
rescue SystemCallError => ex
|
56
|
+
logger.error "There was an system error in method get_new for SSConnection. Details #{ex}:\n#{ex.backtrace}"
|
57
|
+
kill_connection(connection)
|
58
|
+
raise NoFreeConnectionError
|
59
|
+
end
|
60
|
+
|
61
|
+
# Kill SSH connection
|
62
|
+
# +connection+:: SSH connection
|
63
|
+
def kill_connection(connection)
|
64
|
+
connection.shutdown!
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,301 @@
|
|
1
|
+
module AntHill
|
2
|
+
# Node object
|
3
|
+
class Creep
|
4
|
+
|
5
|
+
# +host+:: hostname of creep
|
6
|
+
# +user+:: username to connect
|
7
|
+
# +password+:: password to connect
|
8
|
+
# +status+:: status of creep
|
9
|
+
# +processed+:: number of ants processed
|
10
|
+
# +passed+:: number of successfully processed ants
|
11
|
+
# +start_time+:: status time
|
12
|
+
# +hill_cfg+:: creep configuration
|
13
|
+
# +current_ant+:: currently processing ant
|
14
|
+
attr_reader :host, :user, :password, :status, :processed, :passed, :start_time, :hill_cfg, :current_ant
|
15
|
+
# +active+:: node activness
|
16
|
+
# +custom_data+:: custom data hash
|
17
|
+
# +force_priority+:: If true recalculate priority for this creep instaed of taking cahced values
|
18
|
+
# +current_params+:: current creep configuration
|
19
|
+
# +name+:: creep name
|
20
|
+
attr_accessor :active, :custom_data, :force_priority, :current_params, :name
|
21
|
+
include DRbUndumped
|
22
|
+
|
23
|
+
#Initialize method
|
24
|
+
#[+queen+]:: queen obeject
|
25
|
+
#[+config+]:: configuration
|
26
|
+
def initialize(queen=Queen.queen, config=Configuration.config)
|
27
|
+
@config = config
|
28
|
+
@queen = queen
|
29
|
+
@current_params = {}
|
30
|
+
@custom_data = {}
|
31
|
+
@status = :wait
|
32
|
+
@current_ant = nil
|
33
|
+
@processed = 0
|
34
|
+
@passed = 0
|
35
|
+
@active = true
|
36
|
+
@start_time = Time.now
|
37
|
+
@force_priority = false
|
38
|
+
@modifiers = {}
|
39
|
+
@changed_params = []
|
40
|
+
end
|
41
|
+
|
42
|
+
#Find ant best matching current configuration based on priority
|
43
|
+
def require_ant
|
44
|
+
time = Time.now
|
45
|
+
ant = @queen.find_ant(self)
|
46
|
+
logger.debug "Find min ant took #{Time.now - time}"
|
47
|
+
ant
|
48
|
+
end
|
49
|
+
|
50
|
+
# Return priority of ant for this creep
|
51
|
+
# +ant+:: ant to calculate priority
|
52
|
+
def priority(ant)
|
53
|
+
mod = modifier(ant)
|
54
|
+
ant.prior - mod.get_setup_time(ant)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Initialize instance variables from hash
|
58
|
+
# +hash+:: creep hash
|
59
|
+
def from_hash(hash)
|
60
|
+
@current_params = (hash[:current_parmas] || {})
|
61
|
+
@custom_data = hash[:custom_data] || {}
|
62
|
+
@status = hash[:status] || :wait
|
63
|
+
@processed = hash[:processed] || 0
|
64
|
+
@passed = hash[:passed] || 0
|
65
|
+
@active = hash[:active].nil? ? true : hash[:active]
|
66
|
+
@start_time = hash[:start_time] || Time.now
|
67
|
+
@hill_cfg.merge!(hash[:hill_cfg] || {})
|
68
|
+
end
|
69
|
+
|
70
|
+
# Convert current creep to hash
|
71
|
+
def to_hash
|
72
|
+
{
|
73
|
+
:id => object_id,
|
74
|
+
:current_params => current_params,
|
75
|
+
:custom_data => @custom_data,
|
76
|
+
:status => @status,
|
77
|
+
:processed => @processed,
|
78
|
+
:passed => @passed,
|
79
|
+
:active => @active,
|
80
|
+
:start_time => @start_time,
|
81
|
+
:hill_cfg => @hill_cfg
|
82
|
+
}
|
83
|
+
end
|
84
|
+
|
85
|
+
# Return modifier object for ant
|
86
|
+
# +ant+:: ant for which we need modifier
|
87
|
+
def modifier(ant)
|
88
|
+
@modifiers[ant.type] ||= ant.colony.creep_modifier_class.new(self)
|
89
|
+
end
|
90
|
+
|
91
|
+
# Do setup and run ant
|
92
|
+
# +ant+:: ant to setup and run
|
93
|
+
def setup_and_process_ant(ant)
|
94
|
+
@current_ant = ant
|
95
|
+
@modifier = modifier(ant)
|
96
|
+
ant.start
|
97
|
+
safe do
|
98
|
+
before_process(ant)
|
99
|
+
ok = setup(ant)
|
100
|
+
if ok
|
101
|
+
ant.params.each do |k,v|
|
102
|
+
if !@modifier.creep_params || @modifier.creep_params.include?(k)
|
103
|
+
if current_params[k] != v
|
104
|
+
current_params[k]=v
|
105
|
+
self.force_priority = true
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
run(ant)
|
110
|
+
else
|
111
|
+
setup_failed(ant)
|
112
|
+
self.force_priority = true
|
113
|
+
end
|
114
|
+
end
|
115
|
+
ant.finish
|
116
|
+
safe{ after_process(ant) }
|
117
|
+
if self.force_priority
|
118
|
+
Queen.queen.reset_priority_for_creep(self)
|
119
|
+
self.force_priority = false
|
120
|
+
end
|
121
|
+
@processed+=1
|
122
|
+
@passed +=1 if @current_ant.execution_status.to_sym == :passed
|
123
|
+
@current_ant = nil
|
124
|
+
end
|
125
|
+
|
126
|
+
# Before process hook
|
127
|
+
# +ant+:: Ant object
|
128
|
+
def before_process(ant)
|
129
|
+
@modifier.before_process(ant)
|
130
|
+
end
|
131
|
+
|
132
|
+
# After process hook
|
133
|
+
# +ant+:: Ant object
|
134
|
+
def after_process(ant)
|
135
|
+
@modifier.after_process(ant)
|
136
|
+
end
|
137
|
+
|
138
|
+
# Setup failed hook
|
139
|
+
# +ant+:: Ant object
|
140
|
+
def setup_failed(ant)
|
141
|
+
@modifier.setup_failed(ant)
|
142
|
+
end
|
143
|
+
|
144
|
+
# Setup method
|
145
|
+
# +ant+:: Ant object
|
146
|
+
def setup(ant)
|
147
|
+
timeout = 0
|
148
|
+
timeout = @modifier.get_setup_time(ant)
|
149
|
+
change_status(:setup)
|
150
|
+
ok = false
|
151
|
+
logger.debug "executing setup method with timeout #{timeout}"
|
152
|
+
ok = timeout_execution(timeout, "setup #{ant.params.inspect}", false) do
|
153
|
+
@modifier.setup_ant(ant)
|
154
|
+
end
|
155
|
+
ok &&= timeout_execution( timeout , "check params is #{ant.params.inspect}", false ) do #FIXME: Should we have other value for timeout?
|
156
|
+
@modifier.check(ant)
|
157
|
+
end
|
158
|
+
ok
|
159
|
+
end
|
160
|
+
|
161
|
+
# Run method
|
162
|
+
# +ant+:: Ant object
|
163
|
+
def run(ant)
|
164
|
+
timeout = @modifier.get_run_time(ant)
|
165
|
+
change_status(:run)
|
166
|
+
timeout_execution(timeout, "run #{ant.to_s}") do
|
167
|
+
@modifier.run_ant(ant)
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
# Return logger object
|
172
|
+
def logger
|
173
|
+
Log.logger_for host
|
174
|
+
end
|
175
|
+
|
176
|
+
# Setup creep configuration
|
177
|
+
# +creep_configuration+:: Ant object
|
178
|
+
def configure(creep_configuration)
|
179
|
+
@hill_cfg = creep_configuration
|
180
|
+
@host = @hill_cfg['host']
|
181
|
+
@user = @hill_cfg['user']
|
182
|
+
@password = @hill_cfg['password']
|
183
|
+
@name = @hill_cfg['name']
|
184
|
+
@connection_pool = @config.get_connection_class.new(self)
|
185
|
+
end
|
186
|
+
|
187
|
+
# Execute command on creep
|
188
|
+
# +command+:: Command to run
|
189
|
+
# +timeout+:: Timeout for command. If nil - no timeout
|
190
|
+
def exec!(command, timeout=nil)
|
191
|
+
logger.info("Executing: #{command}")
|
192
|
+
stderr,stdout = '', ''
|
193
|
+
stdout, stderr = timeout_execution(timeout, "exec!(#{command})") do
|
194
|
+
@connection_pool.exec(command)
|
195
|
+
end
|
196
|
+
logger.info("STDERR: #{stderr}") unless stderr.empty?
|
197
|
+
logger.info("STDOUT: #{stdout}") unless stdout.empty?
|
198
|
+
logger.info("Executing done")
|
199
|
+
stdout
|
200
|
+
end
|
201
|
+
|
202
|
+
# Silent version of exec!. Return NoFreeConnection error instance if failed to execute
|
203
|
+
def run_once(command, timeout = nil)
|
204
|
+
exec!(command,timeout)
|
205
|
+
rescue NoFreeConnectionError => ex
|
206
|
+
ex
|
207
|
+
end
|
208
|
+
|
209
|
+
# Execute block with timeout
|
210
|
+
# +timeout+:: Timeout in seconds or nil if no timeout
|
211
|
+
# +process+:: description string, describing process where timeout happened
|
212
|
+
# +default_responce+:: default responce if timeout was raised
|
213
|
+
def timeout_execution(timeout=nil, process = nil, default_response = ['', ''])
|
214
|
+
result = default_response
|
215
|
+
begin
|
216
|
+
Timeout::timeout( timeout ) do
|
217
|
+
result = yield
|
218
|
+
end
|
219
|
+
rescue Timeout::Error => e
|
220
|
+
change_status(:error)
|
221
|
+
logger.error "#{self.host}: timeout error for #{process.to_s}"
|
222
|
+
end
|
223
|
+
result
|
224
|
+
end
|
225
|
+
|
226
|
+
# Create string representation of Creep
|
227
|
+
def to_s
|
228
|
+
took_time = Time.at(Time.now - @start_time).gmtime.strftime('%R:%S')
|
229
|
+
"%s (%i): %s (%s): %s " % [@hill_cfg['host'], @processed, status, took_time, @current_ant]
|
230
|
+
end
|
231
|
+
|
232
|
+
# Retunr if creep is active
|
233
|
+
def active?; @active; end
|
234
|
+
|
235
|
+
# Deactivate creep and set status to +:disabled+
|
236
|
+
def disable!
|
237
|
+
@active = false
|
238
|
+
change_status(:disabled)
|
239
|
+
end
|
240
|
+
|
241
|
+
# Activate creep and set status to +:wait+
|
242
|
+
def enable!
|
243
|
+
@active = true
|
244
|
+
change_status(:wait)
|
245
|
+
end
|
246
|
+
|
247
|
+
# Check if creep is busy
|
248
|
+
# "Free" statuses are +:wait+, +:disabled+, +:error+
|
249
|
+
def busy?
|
250
|
+
!(@status == :wait || @status == :disabled || @status == :error)
|
251
|
+
end
|
252
|
+
|
253
|
+
# Start service
|
254
|
+
def service
|
255
|
+
loop do
|
256
|
+
if !active?
|
257
|
+
logger.debug("Node was disabled")
|
258
|
+
change_status(:disabled)
|
259
|
+
sleep @config.sleep_interval
|
260
|
+
elsif @queen.active? && ant = self.require_ant
|
261
|
+
logger.debug("Setupping and processing ant")
|
262
|
+
setup_and_process_ant(ant)
|
263
|
+
else
|
264
|
+
logger.debug("Waiting for more ants or release")
|
265
|
+
change_status(:wait)
|
266
|
+
sleep @config.sleep_interval
|
267
|
+
end
|
268
|
+
end
|
269
|
+
@connection_pool.destroy
|
270
|
+
end
|
271
|
+
|
272
|
+
# Kill all connections
|
273
|
+
def kill_connections
|
274
|
+
@connection_pool.destroy
|
275
|
+
end
|
276
|
+
|
277
|
+
# Change status and timer for this status of creep
|
278
|
+
def change_status(status)
|
279
|
+
unless @status == status
|
280
|
+
@status = status
|
281
|
+
@start_time = Time.now
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
# Execute block without raising errors
|
286
|
+
def safe
|
287
|
+
begin
|
288
|
+
yield
|
289
|
+
rescue NoFreeConnectionError => e
|
290
|
+
disable!
|
291
|
+
custom_data['disabled_reason'] = :no_free_connections
|
292
|
+
custom_data['disabled_description'] = 'Cannot find free connection or create new one'
|
293
|
+
logger.error "#{e}\n#{e.backtrace}"
|
294
|
+
rescue => e
|
295
|
+
change_status(:error)
|
296
|
+
logger.error "#{e}\n#{e.backtrace}"
|
297
|
+
end
|
298
|
+
end
|
299
|
+
end
|
300
|
+
end
|
301
|
+
|