jobmanager 1.0.1 → 1.1.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.
@@ -13,13 +13,13 @@ module JobManager
13
13
  #
14
14
  class ApplicationConfig < ConfigToolkit::BaseConfig
15
15
 
16
- EMAIL_CONDITIONS = [ :always, :never, :on_failure ]
16
+ EMAIL_CONDITIONS = [ :always, :never, :on_failure, :on_job_output_or_failure ]
17
17
  CENTRAL_LOG_MODES = [ :syslog, :file ]
18
18
 
19
19
  add_required_param(:command, String)
20
20
 
21
21
  add_required_param(:job_name, String)
22
-
22
+
23
23
  add_required_param(:job_logs_directory, Pathname)
24
24
 
25
25
  add_optional_param(:email_condition, Symbol, :on_failure) do |value|
@@ -71,9 +71,21 @@ module JobManager
71
71
  "jobmanager results for <%=job_name%> on <%=host_name%> : <%=result%>")
72
72
 
73
73
  add_optional_param(:email_from_address, String)
74
-
74
+
75
75
  add_optional_param(:email_to_address, String)
76
76
 
77
+ add_optional_param(:rotate_job_log, ConfigToolkit::Boolean, true)
78
+
79
+ add_optional_param(:job_log_prepend_date_time, ConfigToolkit::Boolean, false)
80
+
81
+ add_optional_param(:job_log_prepend_date_time_format, String, '%F_%T') do |value|
82
+ begin
83
+ DateTime.now.strftime(value)
84
+ rescue => e
85
+ raise_error("Invalid format #{e.message}")
86
+ end
87
+ end
88
+
77
89
  def validate_all_values()
78
90
  if (self.central_log_mode == :file && !self.central_log_file?)
79
91
  raise_error("If central_log_mode is set to file, central_log_file must be specified.")
@@ -22,7 +22,7 @@ module JobManager
22
22
  # The command line arguments.
23
23
  # ====Returns:
24
24
  # A hash of the command line options and arugments.
25
- #
25
+ #
26
26
  def self.parse(program_name, args)
27
27
  options = {}
28
28
  opts = OptionParser.new
@@ -89,9 +89,23 @@ module JobManager
89
89
  "Timeout (in seconds) after which the script is killed.") do |timeout|
90
90
  options["timeout"] = timeout
91
91
  end
92
-
92
+
93
+ opts.on("-p", "--[no-]job_log_prepend_date_time",
94
+ "Whether to prepend the date/time to each line of the job log file.") do |value|
95
+ options["job_log_prepend_date_time"] = value
96
+ end
97
+
98
+ opts.on("-g", "--job_log_prepend_date_time_format FORMAT",
99
+ "The format of the date/time to be prepended to each line of the job log file.") do |format|
100
+ options["job_log_prepend_date_time_format"] = format
101
+ end
102
+
103
+ opts.on("-b", "--[no-]rotate_job_log",
104
+ "Whether the job log file should be rotated.") do |value|
105
+ options["rotate_job_log"] = value
106
+ end
107
+
93
108
  opts.on("-e", "--email_settings_file PATH",
94
- String,
95
109
  "The configuration file for the simpleemail gem.",
96
110
  "Note: If a relative path is specified, it will be considered to be relative to the",
97
111
  "location of the configuration file.") do |file|
@@ -102,8 +116,8 @@ module JobManager
102
116
  "The email address from which jobmanager sends results.") do |address|
103
117
  options["email_from_address"] = address
104
118
  end
105
-
106
- opts.on("-o", "--email_to_address ADDRESS",
119
+
120
+ opts.on("-u", "--email_to_address ADDRESS",
107
121
  "The email address to which jobmanager sends results.") do |address|
108
122
  options["email_to_address"] = address
109
123
  end
@@ -0,0 +1,65 @@
1
+ module JobManager
2
+
3
+ #
4
+ # This class represents a stream that wraps a file stream.
5
+ # Its only purpose is to defer the opening of the file to
6
+ # first write. Thus, if the file is never written to, it
7
+ # is never created!
8
+ #
9
+ class OpenOnFirstWriteFile
10
+
11
+ def initialize(file_name, mode_string)
12
+ @file_name = file_name
13
+ @mode_string = mode_string
14
+ @stream = nil
15
+ end
16
+
17
+ def close()
18
+ # This method needs to be explicitly defined. If it wasn't,
19
+ # method_missing would create the file in order to close it,
20
+ # defeating the purpose of this class.
21
+
22
+ if (@stream)
23
+ @stream.close()
24
+ end
25
+ end
26
+
27
+ def flush()
28
+ # This method needs to be explicitly defined. If it wasn't,
29
+ # method_missing would create the file in order to flush it,
30
+ # defeating the purpose of this class.
31
+
32
+ if (@stream)
33
+ @stream.flush()
34
+ end
35
+ end
36
+
37
+ #
38
+ # ====Description:
39
+ # This method first opens the file (if not already opened), and then
40
+ # forwards the call to the file stream.
41
+ #
42
+ def method_missing(*args, &block)
43
+ method = args.shift
44
+
45
+ if (!@stream)
46
+ directory = File.dirname(@file_name)
47
+ begin
48
+ FileUtils.mkdir_p(directory)
49
+ rescue => e
50
+ raise "Failed to create directory #{directory}, Error (#{e.class}): #{e.message}"
51
+ end
52
+
53
+ begin
54
+ @stream = File.new(@file_name, @mode_string)
55
+ rescue => e
56
+ raise "Failed to open file #{@file_name}, Error (#{e.class}): #{e.message}"
57
+ end
58
+ end
59
+
60
+ @stream.send(method, *args)
61
+ end
62
+ end
63
+
64
+ end
65
+
@@ -62,6 +62,7 @@ module JobManager
62
62
  read_pipe, write_pipe = IO.pipe
63
63
  original_time = Time.now()
64
64
  success = false
65
+ job_log_write_success = true
65
66
  timeout = optional_args[:timeout]
66
67
  env_path = optional_args[:command_path] ? optional_args[:command_path] : ENV["PATH"]
67
68
 
@@ -122,7 +123,12 @@ module JobManager
122
123
  end
123
124
 
124
125
  if (bytes.length() > 0)
125
- log_stream << bytes
126
+ begin
127
+ log_stream << bytes
128
+ rescue => e
129
+ logger.error("Error writing to job log: #{e.message}")
130
+ job_log_write_success = false
131
+ end
126
132
  end
127
133
  end
128
134
 
@@ -133,20 +139,21 @@ module JobManager
133
139
  if (Process.waitpid(child_pid, Process::WNOHANG))
134
140
 
135
141
  # success? will return nil if the child didn't exit normally
136
- success = $CHILD_STATUS.success?() ? true : false;
142
+ child_success = $CHILD_STATUS.success?() ? true : false;
137
143
  pid = $CHILD_STATUS.pid()
138
144
 
139
- if (success)
140
- logger.info("Exited successfully")
145
+ if (child_success)
146
+ logger.info("Job exited successfully")
141
147
  else
142
148
  if ($CHILD_STATUS.exited?())
143
- logger.error("Failed! - exited normally with exit code: #{$CHILD_STATUS.exitstatus()}.")
149
+ logger.error("Failed! - Job exited normally with exit code: #{$CHILD_STATUS.exitstatus()}.")
144
150
  elsif ($CHILD_STATUS.signaled?())
145
- logger.error("Failed! - exited abnormally due to signal: #{$CHILD_STATUS.termsig()}.")
151
+ logger.error("Failed! - Job exited abnormally due to signal: #{$CHILD_STATUS.termsig()}.")
146
152
  else
147
- logger.error("Failed! - exited abnormally: #{$CHILD_STATUS.inspect()}")
153
+ logger.error("Failed! - Job exited abnormally: #{$CHILD_STATUS.inspect()}")
148
154
  end
149
155
  end
156
+ success = child_success && job_log_write_success
150
157
 
151
158
  break
152
159
  else
@@ -213,7 +220,7 @@ module JobManager
213
220
  exit(1)
214
221
  end
215
222
  end
216
-
223
+
217
224
 
218
225
  #
219
226
  # ====Description:
@@ -6,8 +6,10 @@ module JobManager
6
6
  # This class represents a stream whose only purpose is to forward
7
7
  # messages on to a list of configured streams.
8
8
  #
9
- class TeeStream < IO
9
+ class TeeStream
10
10
 
11
+ attr_reader :streams
12
+
11
13
  #
12
14
  # ====Description:
13
15
  # This method creates an TeeStream instance initialized with the
@@ -30,49 +32,29 @@ module JobManager
30
32
  # The name of the stream requested.
31
33
  # ====Returns:
32
34
  # The associated stream if it exists, nil otherwise
33
- def get_stream(name)
35
+ def stream(name)
34
36
  @streams[name]
35
37
  end
36
38
 
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
39
+ def method_missing(*args, &block)
40
+ is_exception_saved = false
41
+ saved_name = nil
54
42
 
55
- str.length
43
+ saved_e = nil
44
+ @streams.each do |name, stream|
45
+ begin
46
+ stream.send(*args)
47
+ rescue => saved_e
48
+ is_exception_saved = true
49
+ saved_name = name
50
+ end
51
+ end
52
+
53
+ if (is_exception_saved)
54
+ @streams.delete(saved_name)
55
+ raise "Caught exception on stream #{saved_name}, Error (#{saved_e.class}): #{saved_e.message}"
56
+ end
56
57
  end
57
58
 
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
59
  end
78
60
  end
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'date'
4
+
5
+ module JobManager
6
+
7
+ #
8
+ # This class represents a stream whose only purpose is to forward
9
+ # messages on to a list of configured streams.
10
+ #
11
+ class TimestampedStream < IO
12
+
13
+ attr_reader :stream
14
+ #
15
+ # ====Description:
16
+ # This method creates an TeeStream instance initialized with the
17
+ # hash of streams passed in. When a message is written to this
18
+ # stream, it will be forwarded to all hash values passed in.
19
+ # ====Parameters:
20
+ # [streams]
21
+ # A hash table of stream name to stream.
22
+ #
23
+ def initialize(stream,
24
+ date_time_format = "%F_%T")
25
+ @stream = stream
26
+ @date_time_format = date_time_format
27
+ @is_start_of_line = true
28
+ end
29
+
30
+ #
31
+ # ====Description:
32
+ # This method forwards the string message to all configured
33
+ # streams. Note: This class is derived from IO, and thus only
34
+ # needs to implement the write method in order for all of the
35
+ # write-related methods (<<, print, puts, putc, etc) to work free
36
+ # of charge, as all of these methods call write.
37
+ # ====Parameters:
38
+ # [str]
39
+ # The string to be written.
40
+ # ====Returns:
41
+ # The length of the string.
42
+ #
43
+ def write(str)
44
+ if (str.length() == 0)
45
+ return 0
46
+ end
47
+
48
+ str = str.dup()
49
+ now = DateTime.now().strftime(@date_time_format)
50
+
51
+ # if string ends with a new line, strip it off and save it.
52
+ if (str.sub!(/\n\z/, ""))
53
+ ends_with_new_line = true
54
+ end
55
+
56
+ # replace all of the new lines with a new line followed by the
57
+ # current date/time.
58
+ str.gsub!(/\n/, "\n#{now} ")
59
+
60
+ if (@is_start_of_line)
61
+ str = "#{now} #{str}"
62
+ @is_start_of_line = false
63
+ end
64
+
65
+ if (ends_with_new_line)
66
+ str += "\n"
67
+ @is_start_of_line = true
68
+ end
69
+
70
+ @stream.write(str)
71
+ end
72
+
73
+ #
74
+ # ====Description:
75
+ # Flush all the configured streams.
76
+ #
77
+ def flush
78
+ @stream.flush()
79
+ end
80
+
81
+ #
82
+ # ====Description:
83
+ # Close all the configured streams.
84
+ #
85
+ def close
86
+ @stream.close()
87
+ end
88
+ end
89
+ end
@@ -30,4 +30,10 @@ command: "echo hello"
30
30
 
31
31
  email_from_address: "sender_address@gmail.com"
32
32
 
33
- email_to_address: "receiver_address@gmail.com"
33
+ email_to_address: "receiver_address@gmail.com"
34
+
35
+ rotate_job_log: false
36
+
37
+ job_log_prepend_date_time: true
38
+
39
+ job_log_prepend_date_time_format: '%F'
@@ -4,6 +4,6 @@ central_log_mode: file
4
4
 
5
5
  central_log_file: temp/logs/jobmanager.log
6
6
 
7
- email_condition: on_failure
7
+ email_condition: on_job_output_or_failure
8
8
 
9
9
  email_settings_file: email_settings.rb
@@ -0,0 +1,9 @@
1
+ job_logs_directory: temp/logs
2
+
3
+ central_log_mode: file
4
+
5
+ central_log_file: temp/logs/jobmanager.log
6
+
7
+ email_condition: on_job_output_or_failure
8
+
9
+ email_settings_file: email_settings.rb
@@ -0,0 +1,15 @@
1
+ job_logs_directory: temp/logs
2
+
3
+ central_log_mode: file
4
+
5
+ central_log_file: temp/logs/jobmanager.log
6
+
7
+ rotate_job_log: false
8
+
9
+ email_condition: on_job_output_or_failure
10
+
11
+ email_settings_file: email_settings.rb
12
+
13
+ job_log_prepend_date_time: true
14
+
15
+ job_log_prepend_date_time_format: '%F'
@@ -1,5 +1,8 @@
1
1
  require 'jobmanager/applicationlogger.rb'
2
2
 
3
+ require 'rubygems'
4
+ require 'relative'
5
+
3
6
  require 'test/unit'
4
7
  require_relative('mock_syslog')
5
8
  require_relative('helpers')
@@ -51,6 +51,9 @@ class ConfigTest < Test::Unit::TestCase
51
51
  assert_equal("echo hello", config.command)
52
52
  assert_equal("sender_address@gmail.com", config.email_from_address)
53
53
  assert_equal("receiver_address@gmail.com", config.email_to_address)
54
+ assert_equal(false, config.rotate_job_log)
55
+ assert_equal(true, config.job_log_prepend_date_time)
56
+ assert_equal("%F", config.job_log_prepend_date_time_format)
54
57
 
55
58
  result = 'Success'
56
59
  host_name = "jackfruit"
@@ -77,11 +80,13 @@ class ConfigTest < Test::Unit::TestCase
77
80
  assert_equal(nil, config.central_log_file)
78
81
  assert_equal("jobmanager results for <%=job_name%> on <%=host_name%> : <%=result%>",
79
82
  config.email_subject)
80
- assert_equal(ENV['PATH'], config.command_path)
81
-
83
+ assert_equal(ENV['PATH'], config.command_path)
82
84
  assert_equal(nil, config.timeout)
83
85
  assert_equal(nil, config.email_to_address)
84
86
  assert_equal(nil, config.email_from_address)
87
+ assert_equal(true, config.rotate_job_log)
88
+ assert_equal(false, config.job_log_prepend_date_time)
89
+ assert_equal("%F_%T", config.job_log_prepend_date_time_format)
85
90
  end
86
91
 
87
92
  def test_good_configs
@@ -101,7 +106,7 @@ class ConfigTest < Test::Unit::TestCase
101
106
  confirm_bad_config_throws_error("bad_number_of_job_logs.yaml", msg)
102
107
 
103
108
  msg = "error setting email_condition with value invalid: "
104
- msg << "email_condition must be one of the following values: always, never, on_failure."
109
+ msg << "email_condition must be one of the following values: always, never, on_failure, on_job_output_or_failure."
105
110
  confirm_bad_config_throws_error("bad_email_condition.yaml", msg)
106
111
 
107
112
  msg = "JobManager::ApplicationConfig#validate_all_values error: "
@@ -140,25 +145,28 @@ class ConfigTest < Test::Unit::TestCase
140
145
 
141
146
  # match the config specified in all_values.yaml
142
147
  email_subject = "jobmanager results for <%=job_name%> on <%=host_name%> : <%=result%>"
143
- args = [ "--job_name", "ze_echoer",
144
- "--job_logs_directory", "temp/logs",
145
- "--email_condition", "always",
146
- "--central_log_mode", "file",
147
- "--central_log_file", "temp/logs/jobmanager.log",
148
- "--number_of_job_logs", "4",
148
+ args = [ "--job_name", "ze_echoer",
149
+ "--job_logs_directory", "temp/logs",
150
+ "--email_condition", "always",
151
+ "--central_log_mode", "file",
152
+ "--central_log_file", "temp/logs/jobmanager.log",
153
+ "--number_of_job_logs", "4",
149
154
  "--date_time_extension",
150
- "--date_time_extension_format", "%F",
155
+ "--date_time_extension_format", "%F",
151
156
  "--zip_rotated_log_file",
152
- "--timeout", "1800",
153
- "--email_settings_file", "email_settings.rb",
154
- "--email_from_address", "sender_address@gmail.com",
155
- "--email_to_address", "receiver_address@gmail.com",
156
- "--command_path", "/usr/designingpatterns/bin",
157
- "--email_subject", email_subject,
157
+ "--timeout", "1800",
158
+ "--email_settings_file", "email_settings.rb",
159
+ "--email_from_address", "sender_address@gmail.com",
160
+ "--email_to_address", "receiver_address@gmail.com",
161
+ "--command_path", "/usr/designingpatterns/bin",
162
+ "--email_subject", email_subject,
163
+ "--no-rotate_job_log",
164
+ "--job_log_prepend_date_time",
165
+ "--job_log_prepend_date_time_format", "%F",
158
166
  "--debug",
159
167
  "echo hello"
160
168
  ]
161
-
169
+
162
170
  hash = JobManager::ApplicationOptionParser.parse(program_name, args)
163
171
  config = JobManager::ApplicationConfig.load(ConfigToolkit::HashReader.new(hash))
164
172
  confirm_all_values(config)
@@ -185,12 +193,15 @@ class ConfigTest < Test::Unit::TestCase
185
193
  config_file = File.join(CONFIG_DIR, "incomplete_1.yaml")
186
194
  yaml_reader = ConfigToolkit::YAMLReader.new(config_file)
187
195
 
188
- args = [ "--job_name", "ze_echoer",
189
- "--email_to_address", "receiver_address@gmail.com",
190
- "--central_log_file", "temp/logs/jobmanager.log",
191
- "--email_settings_file", "email_settings.rb",
192
- "--timeout", "1800",
196
+ args = [ "--job_name", "ze_echoer",
197
+ "--email_to_address", "receiver_address@gmail.com",
198
+ "--central_log_file", "temp/logs/jobmanager.log",
199
+ "--email_settings_file", "email_settings.rb",
200
+ "--timeout", "1800",
193
201
  "--zip_rotated_log_file",
202
+ "--no-rotate_job_log",
203
+ "--job_log_prepend_date_time",
204
+ "--job_log_prepend_date_time_format", "%F",
194
205
  "--debug",
195
206
  "echo hello"
196
207
  ]