jobmanager 1.0.1 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
  ]