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,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