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,89 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'configtoolkit'
5
+
6
+ module JobManager
7
+
8
+ #
9
+ # This class is a configuration class used to represent the
10
+ # combination of the configurations specified in the jobmanager
11
+ # application's configuration file and the the jobmanager
12
+ # application's command line arguments.
13
+ #
14
+ class ApplicationConfig < ConfigToolkit::BaseConfig
15
+
16
+ EMAIL_CONDITIONS = [ :always, :never, :on_failure ]
17
+ CENTRAL_LOG_MODES = [ :syslog, :file ]
18
+
19
+ add_required_param(:command, String)
20
+
21
+ add_required_param(:job_name, String)
22
+
23
+ add_required_param(:job_logs_directory, Pathname)
24
+
25
+ add_optional_param(:email_condition, Symbol, :on_failure) do |value|
26
+ if (EMAIL_CONDITIONS.index(value) == nil)
27
+ raise_error("email_condition must be one of the following values: #{EMAIL_CONDITIONS.join(', ')}")
28
+ end
29
+ end
30
+
31
+ add_optional_param(:central_log_mode, Symbol, :syslog) do |value|
32
+ if (CENTRAL_LOG_MODES.index(value) == nil)
33
+ modes = CENTRAL_LOG_MODES.join(', ')
34
+ raise_error("central_log_mode must be one of the following values: #{modes}")
35
+ end
36
+ end
37
+
38
+ add_optional_param(:number_of_job_logs, Fixnum, 3) do |value|
39
+ if (value <= 0)
40
+ raise_error "value must be > 0"
41
+ end
42
+ end
43
+
44
+ add_optional_param(:date_time_extension, ConfigToolkit::Boolean, true)
45
+
46
+ add_optional_param(:date_time_extension_format, String, '%F_%T') do |value|
47
+ begin
48
+ DateTime.now.strftime(value)
49
+ rescue => e
50
+ raise_error("Invalid format #{e.message}")
51
+ end
52
+ end
53
+
54
+ add_optional_param(:zip_rotated_log_file, ConfigToolkit::Boolean, false)
55
+
56
+ add_optional_param(:debug, ConfigToolkit::Boolean, false)
57
+
58
+ add_optional_param(:central_log_file, Pathname)
59
+
60
+ add_optional_param(:command_path, String, ENV['PATH'])
61
+
62
+ add_optional_param(:timeout, Fixnum) do |value|
63
+ if (value <= 0)
64
+ raise_error("timeout must be a positive integer (seconds)")
65
+ end
66
+ end
67
+
68
+ add_optional_param(:email_settings_file, Pathname, "email_settings.rb")
69
+
70
+ add_optional_param(:email_subject, String,
71
+ "jobmanager results for <%=job_name%> on <%=host_name%> : <%=result%>")
72
+
73
+ add_optional_param(:email_from_address, String)
74
+
75
+ add_optional_param(:email_to_address, String)
76
+
77
+ def validate_all_values()
78
+ if (self.central_log_mode == :file && !self.central_log_file?)
79
+ raise_error("If central_log_mode is set to file, central_log_file must be specified.")
80
+ end
81
+
82
+ if (self.central_log_mode == :syslog && self.central_log_file?)
83
+ raise_error("If central_log_mode is set to syslog, central_log_file should not be specified.")
84
+ end
85
+ end
86
+
87
+ end
88
+ end
89
+
@@ -0,0 +1,306 @@
1
+ require 'syslog'
2
+ require 'logger'
3
+ require 'etc'
4
+
5
+ require 'rubygems'
6
+ require 'syslog_logger'
7
+
8
+
9
+ module JobManager
10
+
11
+ #
12
+ # This class is a base class for all of the ApplicationLogger*
13
+ # classes implemented in this file. This class should not be
14
+ # instantiated directly. This class derives from IO so that it can
15
+ # reuse the print methods (print, <<, puts, etc).
16
+ #
17
+ class ApplicationLogger < IO
18
+
19
+ #
20
+ # This constant is a map between the shortcut methods in the
21
+ # Logger interface (debug, info, warn, etc) and their associated
22
+ # severity constants.
23
+ #
24
+ LEVEL_LOGGER_MAP = SyslogLogger::LOGGER_LEVEL_MAP.invert
25
+
26
+ attr_accessor :job_name
27
+ attr_accessor :user_name
28
+
29
+ def initialize(job_name, user_name)
30
+ @job_name = job_name ? job_name : "UNKNOWN"
31
+ @user_name = user_name
32
+ end
33
+
34
+ #
35
+ # ====Description:
36
+ # This method logs the given exception.
37
+ #
38
+ def record_exception(e)
39
+ error(e.message)
40
+ debug(e.backtrace.join("\n"))
41
+ end
42
+
43
+ #
44
+ # ====Description:
45
+ # This method logs the given exception and the associated tag.
46
+ #
47
+ def record_tagged_exception(message, e)
48
+ error("#{message}: Error (#{e.class}): #{e.message}")
49
+ debug(e.backtrace.join("\n"))
50
+ end
51
+
52
+ #
53
+ # ====Description:
54
+ # This method logs a message with severity level Logger::INFO.
55
+ #
56
+ def write(message)
57
+ info(message)
58
+ end
59
+
60
+ end
61
+
62
+ #
63
+ # This class provides a Logger interface and logs messages to syslog
64
+ # via the SysLogLogger gem. In addition, this class provides a
65
+ # string instance method which allows the caller to retrieve a
66
+ # concatenation of all the messages written to this logger. The
67
+ # level, user name, and job name are prepended to each message that
68
+ # is written to syslog. A number of the interface methods are
69
+ # implemented via the method_missing method.
70
+ #
71
+ class ApplicationSyslogLogger < ApplicationLogger
72
+ #
73
+ # ====Description:
74
+ # This method creates a new instance.
75
+ #
76
+ def initialize(program_name,
77
+ user_name,
78
+ job_name = nil)
79
+
80
+ super(job_name, user_name)
81
+
82
+ @program_name = program_name
83
+ @stringio = StringIO.new
84
+
85
+ @logger = SyslogLogger.new(program_name)
86
+ end
87
+
88
+ #
89
+ # ====Returns:
90
+ # A string containing all mesages that were logged through this
91
+ # interface.
92
+ #
93
+ def string()
94
+ return @stringio.string()
95
+ end
96
+
97
+ #
98
+ # ====Description:
99
+ # This method is akin to the Logger::add interface method.
100
+ #
101
+ def add(severity, message = nil, &block)
102
+ wrap_record_send(LEVEL_LOGGER_MAP[severity], message || yield)
103
+ end
104
+ alias log add
105
+
106
+ #
107
+ # ====Description:
108
+ # This method closes the connection to the syslog daemon.
109
+ #
110
+ def close
111
+ if (Syslog.opened?) then Syslog.close() end
112
+ end
113
+
114
+ private
115
+
116
+ #
117
+ # ====Description:
118
+ # This method is intended to handle all shortcut methods (debug,
119
+ # warn, info, etc.), as well as the level attribute methods.
120
+ #
121
+ def method_missing(*args, &block)
122
+ method = args.shift
123
+
124
+ # if the method is a shortcut log method
125
+ if (SyslogLogger::LOGGER_MAP.find() {|key, value| method == key})
126
+ # wrap the message (with the job and user names)
127
+ wrap_record_send(method, (args.length > 0) ? args[0] : yield)
128
+ else
129
+ # otherwise forward the message directly on to the SyslogLogger instance.
130
+ @logger.send(method, *args, &block)
131
+ end
132
+ end
133
+
134
+ def wrap(method, message)
135
+ level = method.to_s.upcase
136
+ return "#{level} [#{user_name()}, #{job_name()}] #{message}"
137
+ end
138
+
139
+ def wrap_record_send(method, message)
140
+ if (!message) then return end
141
+
142
+ # if the level of the message is within the level of the logger
143
+ if (SyslogLogger::LOGGER_LEVEL_MAP[method] >= @logger.level)
144
+ lines = message.split("\n")
145
+
146
+ lines.each do |line|
147
+ wrapped_line = wrap(method, line)
148
+ @logger.send(method, wrapped_line)
149
+ @stringio << wrapped_line << "\n"
150
+ end
151
+ end
152
+ end
153
+ end
154
+
155
+ #
156
+ # This class provides a Logger interface and logs messages to an IO
157
+ # stream. In addition, it provides a string instance method which
158
+ # allows the caller to retrieve a concatenation of all the messages
159
+ # written to this logger (with additional formatting such that the
160
+ # string represents exactly what was written to the IO stream).
161
+ # This class is implemented using the Logger class itself. The user
162
+ # name, and job name are prepended to each message, which is then
163
+ # written via the Logger class (to a string stream) which is in turn
164
+ # written to the IO stream. This was done so that the exact string
165
+ # that was logged to the IO stream could be later returned via the
166
+ # string instance method. A number of the interface methods are
167
+ # implemented via the method_missing method.
168
+ #
169
+ class ApplicationIOLogger < ApplicationLogger
170
+
171
+ LOGGER_LEVELS = [ :debug,
172
+ :info,
173
+ :warn,
174
+ :error,
175
+ :fatal,
176
+ :unknown
177
+ ]
178
+
179
+ #
180
+ # ====Description:
181
+ # This method creates a new instance.
182
+ #
183
+ def initialize(io_stream,
184
+ user_name,
185
+ job_name = nil)
186
+
187
+ super(job_name, user_name)
188
+
189
+ @copy = StringIO.new
190
+ @io_stream = io_stream
191
+
192
+ @logger_stringio = StringIO.new
193
+ @logger = Logger.new(@logger_stringio)
194
+ @logger.formatter = Logger::Formatter.new
195
+ @logger.datetime_format = '%FT%T '
196
+ end
197
+
198
+ #
199
+ # ====Returns:
200
+ # A string containing all mesages that were logged through this
201
+ # interface.
202
+ #
203
+ def string()
204
+ return @copy.string()
205
+ end
206
+
207
+ #
208
+ # ====Description:
209
+ # This method is akin to the Logger::add interface method.
210
+ #
211
+ def add(severity, message = nil, &block)
212
+ wrap_record_send(LEVEL_LOGGER_MAP[severity], message || yield)
213
+ end
214
+ alias log add
215
+
216
+ #
217
+ # ====Description:
218
+ # This method exists only to complete the Logger interface. It
219
+ # is a noop.
220
+ #
221
+ def close
222
+ end
223
+
224
+ private
225
+ def wrap(message)
226
+ if (!message) then return nil end
227
+
228
+ return "[#{user_name()}, #{job_name()}] #{message}"
229
+ end
230
+
231
+ #
232
+ # ====Description:
233
+ # This method captures the output of the Logger instance and
234
+ # forwards it to the contained iostream and the aggregated string
235
+ # available for retrieval via the string method.
236
+ #
237
+ def wrap_record_send(method, message)
238
+ lines = message.split("\n")
239
+ lines.each do |line|
240
+ @logger.send(method, wrap(line))
241
+ end
242
+
243
+ output = @logger_stringio.string()
244
+ @logger_stringio.string = ""
245
+
246
+ @io_stream << output
247
+ @copy << output
248
+ end
249
+
250
+ #
251
+ # ====Description:
252
+ # This method is intended to handle all shortcut methods (debug,
253
+ # warn, info, etc.), as well as the level attribute methods.
254
+ #
255
+ def method_missing(*args, &block)
256
+ method = args.shift
257
+
258
+ # if the method is a shortcut log method
259
+ if (LOGGER_LEVELS.find() {|key| method == key})
260
+ wrap_record_send(method, args[0] ? args[0] : yield)
261
+ else
262
+ # forward the method directly onto the Logger instance.
263
+ @logger.send(method, *args, &block)
264
+ end
265
+ end
266
+ end
267
+
268
+ #
269
+ # This class provides a Logger interface and logs messages to a
270
+ # file. In addition, it provides a string instance method which
271
+ # allows the caller to retrieve a concatenation of all the messages
272
+ # written to this logger. A number of the interface methods are
273
+ # implemented via the method_missing method.
274
+ #
275
+ class ApplicationFileLogger < ApplicationIOLogger
276
+
277
+ #
278
+ # ====Description:
279
+ # This method creates a new instance. All further messages
280
+ # written to this instance will be in turn written to the
281
+ # specified log file.
282
+ #
283
+ def initialize(log_file,
284
+ user_name,
285
+ job_name = nil)
286
+
287
+ FileUtils.mkdir_p(File.dirname(log_file))
288
+ @log_file_stream = File.open(log_file, "a")
289
+
290
+ super(@log_file_stream,
291
+ user_name,
292
+ job_name)
293
+ end
294
+
295
+ #
296
+ # ====Description:
297
+ # This method closes the log file stream.
298
+ #
299
+ def close
300
+ @log_file_stream.close()
301
+ end
302
+ end
303
+
304
+ end
305
+
306
+
@@ -0,0 +1,163 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'optparse'
4
+ require 'jobmanager/applicationconfig.rb'
5
+
6
+ module JobManager
7
+
8
+ #
9
+ # This class is composed of the command line argument parsing code
10
+ # for the jobmanager application.
11
+ #
12
+ class ApplicationOptionParser
13
+
14
+ #
15
+ # ====Description:
16
+ # This method parses the command line options and arguments passed
17
+ # in and returns a hash of these values.
18
+ # ====Parameters:
19
+ # [program_name]
20
+ # The program name.
21
+ # [args]
22
+ # The command line arguments.
23
+ # ====Returns:
24
+ # A hash of the command line options and arugments.
25
+ #
26
+ def self.parse(program_name, args)
27
+ options = {}
28
+ opts = OptionParser.new
29
+
30
+ opts.program_name = program_name
31
+ opts.summary_width = 40
32
+ opts.banner = get_usage(program_name)
33
+
34
+ opts.separator ""
35
+ opts.separator "Specific options:"
36
+
37
+ is_job_name_set = false
38
+ opts.on("-j", "--job_name NAME",
39
+ "The name of the job to be run, and thus the basename of the log file.") do |job_name|
40
+ options["job_name"] = job_name
41
+ is_job_name_set = true
42
+ end
43
+
44
+ opts.on("-l", "--job_logs_directory PATH",
45
+ "The directory in which the job logs will be kept.") do |logs_directory|
46
+ options["job_logs_directory"] = logs_directory
47
+ end
48
+
49
+ conditions = JobManager::ApplicationConfig::EMAIL_CONDITIONS.join(', ')
50
+ opts.on("-c", "--email_condition CONDITION",
51
+ "The condition upon which results should be emailed.",
52
+ "Possible values are:(#{conditions}.") do |condition|
53
+ options["email_condition"] = condition
54
+ end
55
+
56
+ modes = JobManager::ApplicationConfig::CENTRAL_LOG_MODES.join(', ')
57
+ opts.on("-m", "--central_log_mode MODE",
58
+ "Possible values are #{modes}.") do |mode|
59
+ options["central_log_mode"] = mode
60
+ end
61
+
62
+ opts.on("-n", "--number_of_job_logs LOGS",
63
+ "Number of log files to be kept per job.") do |number_of_logs|
64
+ options["number_of_job_logs"] = number_of_logs
65
+ end
66
+
67
+ opts.on("-x", "--[no-]date_time_extension",
68
+ "Whether to add a date/time extension to the rotated log file.") do |value|
69
+ options["date_time_extension"] = value
70
+ end
71
+
72
+ opts.on("-f", "--date_time_extension_format FORMAT",
73
+ "The date/time format of the rotated log file extension.") do |format|
74
+ options["date_time_extension_format"] = format
75
+ end
76
+
77
+ opts.on("-z", "--[no-]zip_rotated_log_file",
78
+ "Zip the rotated log file.") do |value|
79
+ options["zip_rotated_log_file"] = value
80
+ end
81
+
82
+ opts.on("-r", "--central_log_file FILE",
83
+ "The central log file to which jobmanager writes.",
84
+ "This field is only valid if central_log_mode is set to \"file\".") do |file|
85
+ options["central_log_file"] = file
86
+ end
87
+
88
+ opts.on("-t", "--timeout TIMEOUT", OptionParser::DecimalInteger,
89
+ "Timeout (in seconds) after which the script is killed.") do |timeout|
90
+ options["timeout"] = timeout
91
+ end
92
+
93
+ opts.on("-e", "--email_settings_file PATH",
94
+ String,
95
+ "The configuration file for the simpleemail gem.",
96
+ "Note: If a relative path is specified, it will be considered to be relative to the",
97
+ "location of the configuration file.") do |file|
98
+ options["email_settings_file"] = file
99
+ end
100
+
101
+ opts.on("-o", "--email_from_address ADDRESS",
102
+ "The email address from which jobmanager sends results.") do |address|
103
+ options["email_from_address"] = address
104
+ end
105
+
106
+ opts.on("-o", "--email_to_address ADDRESS",
107
+ "The email address to which jobmanager sends results.") do |address|
108
+ options["email_to_address"] = address
109
+ end
110
+
111
+ opts.on("-p", "--command_path PATH",
112
+ "The path to search for the command that jobmanager is invoked with.") do |path|
113
+ options["command_path"] = path
114
+ end
115
+
116
+ opts.on("-s", "--email_subject SUBJECT",
117
+ "The email subject, to be interpreted by ERB.",
118
+ "Allowed variables: (job_name, command, host_name, result, user_name)") do |subject|
119
+ options["email_subject"] = subject
120
+ end
121
+
122
+ opts.on_tail("-d", "--[no-]debug", "Turn on/off debug trace.") do |value|
123
+ options["debug"] = value
124
+ end
125
+
126
+ opts.on_tail("-h", "--help", "Show this message.") do
127
+ puts opts, "\n"
128
+ exit(0)
129
+ end
130
+
131
+ opts.parse!(args)
132
+
133
+ if (args.length != 1)
134
+ puts opts, "\n"
135
+ raise ArgumentError, "One argument (command) is required!"
136
+ end
137
+
138
+ options["command"] = args.shift()
139
+
140
+ if (!is_job_name_set)
141
+ exe = options["command"].split(' ')[0]
142
+ options["job_name"] = File.basename(exe)
143
+ end
144
+
145
+ options
146
+ end
147
+
148
+
149
+ def self.get_usage(program_name)
150
+ usage = <<-EOL
151
+
152
+ Usage: #{program_name} [options] <command>
153
+
154
+ See http://jobmanager.rubyforge.com for a more detailed description and usage examples.
155
+
156
+ EOL
157
+
158
+ usage
159
+ end
160
+
161
+
162
+ end
163
+ end