jobmanager 1.0.0
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/FAQ.txt +151 -0
- data/History.txt +2 -0
- data/LICENSE +20 -0
- data/Manifest.txt +42 -0
- data/README.txt +395 -0
- data/Rakefile +17 -0
- data/bin/jobmanager +20 -0
- data/examples/email_settings.rb +14 -0
- data/examples/example.rb +23 -0
- data/examples/jobmanager.yaml +66 -0
- data/examples/mysql_backup.rb +18 -0
- data/lib/jobmanager.rb +1 -0
- data/lib/jobmanager/application.rb +455 -0
- data/lib/jobmanager/applicationconfig.rb +89 -0
- data/lib/jobmanager/applicationlogger.rb +306 -0
- data/lib/jobmanager/applicationoptionparser.rb +163 -0
- data/lib/jobmanager/system.rb +240 -0
- data/lib/jobmanager/teestream.rb +78 -0
- data/lib/jobmanager/util.rb +25 -0
- data/test/configs/all_values.yaml +33 -0
- data/test/configs/bad_email_condition.yaml +7 -0
- data/test/configs/bad_no_central_log_file.yaml +7 -0
- data/test/configs/bad_number_of_job_logs.yaml +7 -0
- data/test/configs/bad_timeout_zero.yaml +7 -0
- data/test/configs/email_settings.rb +16 -0
- data/test/configs/incomplete_1.yaml +23 -0
- data/test/configs/jobmanager_1.yaml +9 -0
- data/test/configs/jobmanager_2.yaml +11 -0
- data/test/configs/jobmanager_3.yaml +13 -0
- data/test/configs/jobmanager_4_bad_central_log_file.yaml +9 -0
- data/test/configs/jobmanager_4_bad_email_settings_file.yaml +9 -0
- data/test/configs/jobmanager_4_bad_job_logs_directory_1.yaml +9 -0
- data/test/configs/jobmanager_4_bad_job_logs_directory_2.yaml +9 -0
- data/test/configs/minimum_plus_email_settings.yaml +9 -0
- data/test/configs/minimum_required.yaml +5 -0
- data/test/helpers.rb +7 -0
- data/test/mock_syslog.rb +68 -0
- data/test/test_applicationlogger.rb +145 -0
- data/test/test_config.rb +208 -0
- data/test/test_jobmanager.rb +443 -0
- data/test/test_system.rb +206 -0
- data/test/test_teestream.rb +55 -0
- metadata +172 -0
@@ -0,0 +1,240 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'English'
|
4
|
+
require 'pathname'
|
5
|
+
|
6
|
+
BANNER = "==============================================================\n"
|
7
|
+
|
8
|
+
module JobManager
|
9
|
+
|
10
|
+
#
|
11
|
+
# This module contains the system programming required to invoke the
|
12
|
+
# job process and record its output and exit status.
|
13
|
+
#
|
14
|
+
module System
|
15
|
+
|
16
|
+
SLEEP_DURATION_PIPE_OPEN_BUT_EMPTY = 0.5
|
17
|
+
SLEEP_DURATION_PIPE_CLOSED = 0.5
|
18
|
+
SLEEP_DURATION_WAIT_AFTER_KILL = 10.0
|
19
|
+
BUFFER_SIZE = 4096
|
20
|
+
|
21
|
+
#
|
22
|
+
# ====Description:
|
23
|
+
# This method forks and executes the command parameter. It writes
|
24
|
+
# the output (stdout and stderr) of the forked process to the
|
25
|
+
# parameter log_stream. This method logs operational information
|
26
|
+
# (when the command is started, finished, the result) via the
|
27
|
+
# logger command.
|
28
|
+
#
|
29
|
+
# The optional arguments include:
|
30
|
+
# timeout::
|
31
|
+
# The timeout (in seconds) after which this process will
|
32
|
+
# timeout and kill the forked process. If this option is not
|
33
|
+
# specified, no timeout will be applied- this process will wait
|
34
|
+
# indefinitely for the forked process to complete.
|
35
|
+
# command_path::
|
36
|
+
# The path this method will search through to find the passed
|
37
|
+
# in command if the command has a relative path. If this
|
38
|
+
# option is not specified, the environment path will be used.
|
39
|
+
#
|
40
|
+
# ====Parameters:
|
41
|
+
# [command]
|
42
|
+
# The command to be invoked.
|
43
|
+
# [log_stream]
|
44
|
+
# The stream to which this method will write the forked
|
45
|
+
# process's output (stdout and stderr).
|
46
|
+
# [logger]
|
47
|
+
# The logger instance to which this method will write
|
48
|
+
# operational information.
|
49
|
+
# [optional_args]
|
50
|
+
# A hash table of optional arguments.
|
51
|
+
# ====Returns:
|
52
|
+
# +true+ if the command was successful, +false+ otherwise.
|
53
|
+
def self.invoke_command(command,
|
54
|
+
log_stream,
|
55
|
+
logger,
|
56
|
+
optional_args = {},
|
57
|
+
&block)
|
58
|
+
|
59
|
+
# Create a pipe which the forked process will write all of its
|
60
|
+
# output to (stdout and stderr), and this process will read said
|
61
|
+
# output from.
|
62
|
+
read_pipe, write_pipe = IO.pipe
|
63
|
+
original_time = Time.now()
|
64
|
+
success = false
|
65
|
+
timeout = optional_args[:timeout]
|
66
|
+
env_path = optional_args[:command_path] ? optional_args[:command_path] : ENV["PATH"]
|
67
|
+
|
68
|
+
# Parse out the executable from the command (eg. command = "ls -l /", exe = "ls")
|
69
|
+
exe = command.split(' ')[0]
|
70
|
+
|
71
|
+
# Search the path for the executable (if the executable has a
|
72
|
+
# relative path), and ensure the file referenced is indeed
|
73
|
+
# executable.
|
74
|
+
if (!(exe_expanded_path = which(exe, env_path)))
|
75
|
+
if (Pathname.new(exe).absolute?())
|
76
|
+
logger.error("File #{exe} does not exist or is not executable!")
|
77
|
+
else
|
78
|
+
logger.error("File #{exe} does not exist in path (#{env_path}) or is not executable!")
|
79
|
+
end
|
80
|
+
return false
|
81
|
+
end
|
82
|
+
|
83
|
+
exe = exe_expanded_path
|
84
|
+
|
85
|
+
# Rebuild the command with the full path of the executable.
|
86
|
+
command_and_args = command.split(' ')
|
87
|
+
command_and_args[0] = exe
|
88
|
+
command = command_and_args.join(' ')
|
89
|
+
|
90
|
+
# Fork a child process which will in turn exec the command.
|
91
|
+
if (child_pid = fork())
|
92
|
+
# The parent process.
|
93
|
+
|
94
|
+
# This is primarily for testing purposes.
|
95
|
+
if (block) then block.call(child_pid) end
|
96
|
+
|
97
|
+
# The parent process will only read from this pipe.
|
98
|
+
write_pipe.close()
|
99
|
+
|
100
|
+
logger.info("Command (#{command}) launched, pid = #{child_pid}.")
|
101
|
+
|
102
|
+
timed_out = false
|
103
|
+
|
104
|
+
# while pipe is open and the timeout hasn't been reached
|
105
|
+
while (!timeout || !(timed_out = Time.now() - original_time > timeout))
|
106
|
+
bytes = ""
|
107
|
+
begin
|
108
|
+
bytes = read_pipe.read_nonblock(BUFFER_SIZE)
|
109
|
+
rescue Errno::EAGAIN
|
110
|
+
# EAGAIN will be raised if there is nothing to read on the
|
111
|
+
# pipe.
|
112
|
+
sleep(SLEEP_DURATION_PIPE_OPEN_BUT_EMPTY)
|
113
|
+
rescue EOFError
|
114
|
+
# EOFError will be raised if the writer has closed the
|
115
|
+
# pipe.
|
116
|
+
break
|
117
|
+
rescue => error
|
118
|
+
logger.error("Unexpected Error (#{error.class}) " +
|
119
|
+
"reading from the pipe associated with child process:" +
|
120
|
+
"#{error.message}")
|
121
|
+
raise
|
122
|
+
end
|
123
|
+
|
124
|
+
if (bytes.length() > 0)
|
125
|
+
log_stream << bytes
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
# while the child is alive and the timeout hasn't been reached
|
130
|
+
while (!timeout || !(timed_out = Time.now() - original_time > timeout))
|
131
|
+
|
132
|
+
# if the child process has exited, check the exit status
|
133
|
+
if (Process.waitpid(child_pid, Process::WNOHANG))
|
134
|
+
|
135
|
+
# success? will return nil if the child didn't exit normally
|
136
|
+
success = $CHILD_STATUS.success?() ? true : false;
|
137
|
+
pid = $CHILD_STATUS.pid()
|
138
|
+
|
139
|
+
if (success)
|
140
|
+
logger.info("Exited successfully")
|
141
|
+
else
|
142
|
+
if ($CHILD_STATUS.exited?())
|
143
|
+
logger.error("Failed! - exited normally with exit code: #{$CHILD_STATUS.exitstatus()}.")
|
144
|
+
elsif ($CHILD_STATUS.signaled?())
|
145
|
+
logger.error("Failed! - exited abnormally due to signal: #{$CHILD_STATUS.termsig()}.")
|
146
|
+
else
|
147
|
+
logger.error("Failed! - exited abnormally: #{$CHILD_STATUS.inspect()}")
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
break
|
152
|
+
else
|
153
|
+
sleep(SLEEP_DURATION_PIPE_CLOSED)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
# if the child process has reached its timeout, kill it.
|
158
|
+
if (timed_out)
|
159
|
+
logger.error("Timed out after #{timeout} seconds.")
|
160
|
+
|
161
|
+
# Kill the child process with the TERM signal first. The
|
162
|
+
# child process can catch this signal and exit gracefully.
|
163
|
+
Process.kill("TERM", child_pid)
|
164
|
+
logger.error("Sent signal TERM to child process.")
|
165
|
+
|
166
|
+
sleep(SLEEP_DURATION_WAIT_AFTER_KILL)
|
167
|
+
|
168
|
+
# Check if the child pid has exited.
|
169
|
+
if (Process.waitpid(child_pid, Process::WNOHANG))
|
170
|
+
logger.error("Reaped pid from child process.")
|
171
|
+
else
|
172
|
+
# The child pid has not exited. Send a KILL signal.
|
173
|
+
# A process can not catch this signal.
|
174
|
+
Process.kill("KILL", child_pid)
|
175
|
+
logger.error("Sent signal KILL to child process.")
|
176
|
+
|
177
|
+
sleep(SLEEP_DURATION_WAIT_AFTER_KILL)
|
178
|
+
|
179
|
+
# Check if the child pid has exited.
|
180
|
+
if (Process.waitpid(child_pid, Process::WNOHANG))
|
181
|
+
logger.error("Reaped pid from child process.")
|
182
|
+
else
|
183
|
+
# The child should've exited already. Either the system
|
184
|
+
# is VERY slow and the OS hasn't gotten around to
|
185
|
+
# tearing down the process, or something very strange is
|
186
|
+
# going on.
|
187
|
+
logger.error("Failed to reap pid from child process.")
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
return success
|
193
|
+
else
|
194
|
+
# The child process.
|
195
|
+
|
196
|
+
# The child process will only write to this pipe.
|
197
|
+
read_pipe.close()
|
198
|
+
|
199
|
+
# Redirect stdout and stderr to the pipe (to be read by the
|
200
|
+
# parent process). Reset $stdout and $stderr, as they may
|
201
|
+
# have earlier been set to something other than STDOUT,STDERR.
|
202
|
+
STDOUT.reopen(write_pipe); $stdout = STDOUT
|
203
|
+
STDERR.reopen(write_pipe); $stderr = STDERR
|
204
|
+
|
205
|
+
# Exec the command!
|
206
|
+
begin
|
207
|
+
exec(command)
|
208
|
+
rescue => e
|
209
|
+
print "Exec failed for command (#{command}), error: #{e}!";
|
210
|
+
end
|
211
|
+
|
212
|
+
# We will only get here if an error was raised.
|
213
|
+
exit(1)
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
|
218
|
+
#
|
219
|
+
# ====Description:
|
220
|
+
# This method searches the path passed in for an executable with
|
221
|
+
# the name passed in. It searches in order and returns the
|
222
|
+
# absolute pathname of the first match.
|
223
|
+
# ====Parameters:
|
224
|
+
# [name]
|
225
|
+
# The name of the executable to search for.
|
226
|
+
# [path]
|
227
|
+
# The path to search.
|
228
|
+
# ====Returns:
|
229
|
+
# The absolute pathname of the first match.
|
230
|
+
#
|
231
|
+
def self.which(name, path = ENV["PATH"])
|
232
|
+
if (Pathname.new(name).absolute?())
|
233
|
+
return (File.file?(name) && File.executable?(name)) ? name : nil
|
234
|
+
else
|
235
|
+
candidates = path.split(':').map { |path| File.join(path, name)}
|
236
|
+
return candidates.find { |file| File.file?(file) && File.executable?(file) }
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
240
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
module JobManager
|
4
|
+
|
5
|
+
#
|
6
|
+
# This class represents a stream whose only purpose is to forward
|
7
|
+
# messages on to a list of configured streams.
|
8
|
+
#
|
9
|
+
class TeeStream < IO
|
10
|
+
|
11
|
+
#
|
12
|
+
# ====Description:
|
13
|
+
# This method creates an TeeStream instance initialized with the
|
14
|
+
# hash of streams passed in. When a message is written to this
|
15
|
+
# stream, it will be forwarded to all hash values passed in.
|
16
|
+
# ====Parameters:
|
17
|
+
# [streams]
|
18
|
+
# A hash table of stream name to stream.
|
19
|
+
#
|
20
|
+
def initialize(streams)
|
21
|
+
@streams = streams
|
22
|
+
end
|
23
|
+
|
24
|
+
#
|
25
|
+
# ====Description:
|
26
|
+
# This method returns the stream associated with the stream name
|
27
|
+
# specified.
|
28
|
+
# ====Parameters:
|
29
|
+
# [name]
|
30
|
+
# The name of the stream requested.
|
31
|
+
# ====Returns:
|
32
|
+
# The associated stream if it exists, nil otherwise
|
33
|
+
def get_stream(name)
|
34
|
+
@streams[name]
|
35
|
+
end
|
36
|
+
|
37
|
+
#
|
38
|
+
# ====Description:
|
39
|
+
# This method forwards the string message to all configured
|
40
|
+
# streams. Note: This class is derived from IO, and thus only
|
41
|
+
# needs to implement the write method in order for all of the
|
42
|
+
# write-related methods (<<, print, puts, putc, etc) to work free
|
43
|
+
# of charge, as all of these methods call write.
|
44
|
+
# ====Parameters:
|
45
|
+
# [str]
|
46
|
+
# The string to be written.
|
47
|
+
# ====Returns:
|
48
|
+
# The length of the string.
|
49
|
+
#
|
50
|
+
def write(str)
|
51
|
+
@streams.each_value do |stream|
|
52
|
+
stream.write(str)
|
53
|
+
end
|
54
|
+
|
55
|
+
str.length
|
56
|
+
end
|
57
|
+
|
58
|
+
#
|
59
|
+
# ====Description:
|
60
|
+
# Flush all the configured streams.
|
61
|
+
#
|
62
|
+
def flush
|
63
|
+
@streams.each_value do |stream|
|
64
|
+
stream.flush()
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
#
|
69
|
+
# ====Description:
|
70
|
+
# Close all the configured streams.
|
71
|
+
#
|
72
|
+
def close
|
73
|
+
@streams.each_value do |stream|
|
74
|
+
stream.close()
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
#!/usr/bin/env/ruby
|
2
|
+
|
3
|
+
module JobManager
|
4
|
+
|
5
|
+
#
|
6
|
+
# ====Description:
|
7
|
+
# Helper method to temporarily disable warnings for the invocation
|
8
|
+
# of the block.
|
9
|
+
# ====Parameters:
|
10
|
+
# [block]
|
11
|
+
# The block to be invoked.
|
12
|
+
#
|
13
|
+
def self.disable_warnings(&block)
|
14
|
+
save = $-w
|
15
|
+
$-w = false
|
16
|
+
|
17
|
+
begin
|
18
|
+
block.call
|
19
|
+
ensure
|
20
|
+
$-w = save
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
@@ -0,0 +1,33 @@
|
|
1
|
+
email_condition: always
|
2
|
+
|
3
|
+
number_of_job_logs: 4
|
4
|
+
|
5
|
+
central_log_mode: file
|
6
|
+
|
7
|
+
job_logs_directory: temp/logs
|
8
|
+
|
9
|
+
central_log_file: temp/logs/jobmanager.log
|
10
|
+
|
11
|
+
command_path: /usr/designingpatterns/bin
|
12
|
+
|
13
|
+
date_time_extension: true
|
14
|
+
|
15
|
+
date_time_extension_format: '%F'
|
16
|
+
|
17
|
+
zip_rotated_log_file: true
|
18
|
+
|
19
|
+
timeout: 1800
|
20
|
+
|
21
|
+
email_settings_file: email_settings.rb
|
22
|
+
|
23
|
+
email_subject: "jobmanager results for <%=job_name%> on <%=host_name%> : <%=result%>"
|
24
|
+
|
25
|
+
debug: true
|
26
|
+
|
27
|
+
job_name: ze_echoer
|
28
|
+
|
29
|
+
command: "echo hello"
|
30
|
+
|
31
|
+
email_from_address: "sender_address@gmail.com"
|
32
|
+
|
33
|
+
email_to_address: "receiver_address@gmail.com"
|
@@ -0,0 +1,16 @@
|
|
1
|
+
ActionMailer::Base.delivery_method = :test
|
2
|
+
|
3
|
+
ActionMailer::Base.smtp_settings = {
|
4
|
+
:address => 'smtp.gmail.com',
|
5
|
+
:port => 587,
|
6
|
+
:domain => "gmail.com",
|
7
|
+
:authentication => "login",
|
8
|
+
:user_name => "sender@gmail.com",
|
9
|
+
:password => "password",
|
10
|
+
:tls => :auto
|
11
|
+
}
|
12
|
+
|
13
|
+
SimpleEmail::Email.default_from = "Sender <sender@gmail.com>"
|
14
|
+
SimpleEmail::Email.default_to = "Receiver <receiver@gmail.com>"
|
15
|
+
|
16
|
+
|
@@ -0,0 +1,23 @@
|
|
1
|
+
email_condition: always
|
2
|
+
|
3
|
+
number_of_job_logs: 4
|
4
|
+
|
5
|
+
central_log_mode: file
|
6
|
+
|
7
|
+
job_logs_directory: temp/logs
|
8
|
+
|
9
|
+
command_path: /usr/designingpatterns/bin
|
10
|
+
|
11
|
+
date_time_extension: true
|
12
|
+
|
13
|
+
date_time_extension_format: '%F'
|
14
|
+
|
15
|
+
zip_rotated_log_file: false
|
16
|
+
|
17
|
+
timeout: 1
|
18
|
+
|
19
|
+
email_subject: "jobmanager results for <%=job_name%> on <%=host_name%> : <%=result%>"
|
20
|
+
|
21
|
+
debug: false
|
22
|
+
|
23
|
+
email_from_address: "sender_address@gmail.com"
|