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