jobmanager 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. data/FAQ.txt +151 -0
  2. data/History.txt +2 -0
  3. data/LICENSE +20 -0
  4. data/Manifest.txt +42 -0
  5. data/README.txt +395 -0
  6. data/Rakefile +17 -0
  7. data/bin/jobmanager +20 -0
  8. data/examples/email_settings.rb +14 -0
  9. data/examples/example.rb +23 -0
  10. data/examples/jobmanager.yaml +66 -0
  11. data/examples/mysql_backup.rb +18 -0
  12. data/lib/jobmanager.rb +1 -0
  13. data/lib/jobmanager/application.rb +455 -0
  14. data/lib/jobmanager/applicationconfig.rb +89 -0
  15. data/lib/jobmanager/applicationlogger.rb +306 -0
  16. data/lib/jobmanager/applicationoptionparser.rb +163 -0
  17. data/lib/jobmanager/system.rb +240 -0
  18. data/lib/jobmanager/teestream.rb +78 -0
  19. data/lib/jobmanager/util.rb +25 -0
  20. data/test/configs/all_values.yaml +33 -0
  21. data/test/configs/bad_email_condition.yaml +7 -0
  22. data/test/configs/bad_no_central_log_file.yaml +7 -0
  23. data/test/configs/bad_number_of_job_logs.yaml +7 -0
  24. data/test/configs/bad_timeout_zero.yaml +7 -0
  25. data/test/configs/email_settings.rb +16 -0
  26. data/test/configs/incomplete_1.yaml +23 -0
  27. data/test/configs/jobmanager_1.yaml +9 -0
  28. data/test/configs/jobmanager_2.yaml +11 -0
  29. data/test/configs/jobmanager_3.yaml +13 -0
  30. data/test/configs/jobmanager_4_bad_central_log_file.yaml +9 -0
  31. data/test/configs/jobmanager_4_bad_email_settings_file.yaml +9 -0
  32. data/test/configs/jobmanager_4_bad_job_logs_directory_1.yaml +9 -0
  33. data/test/configs/jobmanager_4_bad_job_logs_directory_2.yaml +9 -0
  34. data/test/configs/minimum_plus_email_settings.yaml +9 -0
  35. data/test/configs/minimum_required.yaml +5 -0
  36. data/test/helpers.rb +7 -0
  37. data/test/mock_syslog.rb +68 -0
  38. data/test/test_applicationlogger.rb +145 -0
  39. data/test/test_config.rb +208 -0
  40. data/test/test_jobmanager.rb +443 -0
  41. data/test/test_system.rb +206 -0
  42. data/test/test_teestream.rb +55 -0
  43. 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,7 @@
1
+ job_logs_directory: temp/logs
2
+
3
+ job_name: ze_echoer
4
+
5
+ command: "echo hello"
6
+
7
+ email_condition: invalid
@@ -0,0 +1,7 @@
1
+ job_logs_directory: temp/logs
2
+
3
+ job_name: ze_echoer
4
+
5
+ command: "echo hello"
6
+
7
+ central_log_mode: file
@@ -0,0 +1,7 @@
1
+ job_logs_directory: temp/logs
2
+
3
+ job_name: ze_echoer
4
+
5
+ command: "echo hello"
6
+
7
+ number_of_job_logs: 0
@@ -0,0 +1,7 @@
1
+ job_logs_directory: temp/logs
2
+
3
+ job_name: ze_echoer
4
+
5
+ command: "echo hello"
6
+
7
+ timeout: 0
@@ -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"