yore 0.0.2 → 0.0.3
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.
- data/History.txt +12 -0
- data/Manifest.txt +3 -0
- data/bin/yore +6 -2
- data/lib/ihl_ruby/config.rb +107 -0
- data/lib/ihl_ruby/logging.rb +159 -0
- data/lib/ihl_ruby/misc_utils.rb +2 -75
- data/lib/ihl_ruby/shell_extras.rb +84 -0
- data/lib/yore.orig.rb +1 -1
- data/lib/yore/yore_core.rb +74 -186
- data/test/test_job_a.xml +1 -1
- data/test/test_job_b.xml +1 -1
- data/test/yore_test.rb +10 -1
- metadata +5 -2
data/History.txt
CHANGED
@@ -2,3 +2,15 @@
|
|
2
2
|
|
3
3
|
* 1 major enhancement:
|
4
4
|
* Initial release
|
5
|
+
|
6
|
+
== 0.0.2 2009-2-04
|
7
|
+
|
8
|
+
* now using popen4
|
9
|
+
* raises exceptions when commandline tools fail
|
10
|
+
* now using shoulda for tests
|
11
|
+
|
12
|
+
== 0.0.3 2009-02-20
|
13
|
+
|
14
|
+
* much better logging, reporting and console output
|
15
|
+
|
16
|
+
|
data/Manifest.txt
CHANGED
@@ -3,8 +3,11 @@ History.txt
|
|
3
3
|
lib/ihl_ruby/enum.rb
|
4
4
|
lib/ihl_ruby/extend_base_classes.rb
|
5
5
|
lib/ihl_ruby/misc_utils.rb
|
6
|
+
lib/ihl_ruby/config.rb
|
7
|
+
lib/ihl_ruby/logging.rb
|
6
8
|
lib/ihl_ruby/string_utils.rb
|
7
9
|
lib/ihl_ruby/xml_utils.rb
|
10
|
+
lib/ihl_ruby/shell_extras.rb
|
8
11
|
lib/yore.orig.rb
|
9
12
|
lib/yore/yore_core.rb
|
10
13
|
Manifest.txt
|
data/bin/yore
CHANGED
@@ -24,9 +24,10 @@ def command(aParser,aController,aAction,aShortDescription=nil,aOptionParser=nil,
|
|
24
24
|
c.options = aOptionParser if aOptionParser
|
25
25
|
c.set_execution_block do |args|
|
26
26
|
job = args.first
|
27
|
+
aController.logger.info "Job file: #{File.expand_path(job)}"
|
27
28
|
xmlRoot = XmlUtils.get_file_root(job)
|
28
|
-
|
29
|
-
|
29
|
+
aController.configure(xmlRoot,CMD_OPTIONS,{:basepath => File.dirname(File.expand_path(job))})
|
30
|
+
aController.do_action(aAction,args)
|
30
31
|
end
|
31
32
|
aParser.add_command(c)
|
32
33
|
end
|
@@ -63,3 +64,6 @@ command(cmd,yore,:db_dump,"Dump database by name in job\n")
|
|
63
64
|
|
64
65
|
cmd.parse
|
65
66
|
|
67
|
+
yore.logger.info "\nComplete.\n"
|
68
|
+
yore.report
|
69
|
+
|
@@ -0,0 +1,107 @@
|
|
1
|
+
class ConfigClass < Hash
|
2
|
+
|
3
|
+
attr_reader :default_values
|
4
|
+
|
5
|
+
def initialize(aDefaultValues,aNewValues=nil,&aBlock)
|
6
|
+
self.merge!(@default_values = aDefaultValues.clone)
|
7
|
+
if aNewValues
|
8
|
+
block_given? ? read(aNewValues,&aBlock) : read(aNewValues)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
# aBlock allows values to be filtered based on key,default and new values
|
13
|
+
def read(aSource,&aBlock)
|
14
|
+
default_values.each do |k,v|
|
15
|
+
done = false
|
16
|
+
if block_given? && ((newv = yield(k,v,aSource && aSource[k])) != nil)
|
17
|
+
self[k] = newv
|
18
|
+
done = true
|
19
|
+
end
|
20
|
+
copy_item(aSource,k) if !done && aSource && aSource[k]
|
21
|
+
end
|
22
|
+
self
|
23
|
+
end
|
24
|
+
|
25
|
+
# reset values back to defaults
|
26
|
+
def reset
|
27
|
+
self.clear
|
28
|
+
self.merge!(default_values)
|
29
|
+
end
|
30
|
+
|
31
|
+
def set_int(aKey,aValue)
|
32
|
+
case aValue
|
33
|
+
when String then self[aKey] = aValue.to_integer(self[aKey]);
|
34
|
+
when Fixnum then self[aKey] = aValue;
|
35
|
+
when Float then self[aKey] = aValue.to_i;
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def set_float(aKey,aValue)
|
40
|
+
case aValue
|
41
|
+
when String then self[aKey] = aValue.to_float(self[aKey]);
|
42
|
+
when Fixnum then self[aKey] = aValue.to_f;
|
43
|
+
when Float then self[aKey] = aValue;
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def set_boolean(aKey,aValue)
|
48
|
+
case aValue
|
49
|
+
when TrueClass,FalseClass then self[aKey] = aValue;
|
50
|
+
when String then self[aKey] = (['1','yes','y','true','on'].include?(aValue.downcase))
|
51
|
+
else
|
52
|
+
set_boolean(aKey,aValue.to_s)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def set_symbol(aKey,aValue)
|
57
|
+
case aValue
|
58
|
+
when String then self[aKey] = (aValue.to_sym rescue nil);
|
59
|
+
when Symbol then self[aKey] = aValue;
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def copy_item(aHash,aKey)
|
64
|
+
case default_values[aKey]
|
65
|
+
when NilClass then ;
|
66
|
+
when String then self[aKey] = aHash[aKey].to_s unless aHash[aKey].nil?
|
67
|
+
when Float then set_float(aKey,aHash[aKey]);
|
68
|
+
when Fixnum then set_int(aKey,aHash[aKey]);
|
69
|
+
when TrueClass, FalseClass then set_boolean(aKey,aHash[aKey]);
|
70
|
+
when Symbol then self[aKey] = (aHash[aKey].to_sym rescue nil)
|
71
|
+
else
|
72
|
+
raise Error.new('unsupported type')
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def copy_strings(aHash,*aKeys)
|
77
|
+
aKeys.each do |k|
|
78
|
+
self[k] = aHash[k].to_s unless aHash[k].nil?
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def copy_ints(*aDb)
|
83
|
+
aHash = aDb.shift
|
84
|
+
aKeys = aDb
|
85
|
+
aKeys.each do |k|
|
86
|
+
set_int(k,aHash[k])
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def copy_floats(aHash,*aKeys)
|
91
|
+
aKeys.each do |k|
|
92
|
+
set_float(k,aHash[k])
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def copy_booleans(aHash,*aKeys)
|
97
|
+
aKeys.each do |k|
|
98
|
+
set_boolean(k,aHash[k])
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def to_hash
|
103
|
+
{}.merge(self)
|
104
|
+
end
|
105
|
+
|
106
|
+
end
|
107
|
+
|
@@ -0,0 +1,159 @@
|
|
1
|
+
require 'logger'
|
2
|
+
require 'ihl_ruby/misc_utils'
|
3
|
+
|
4
|
+
class Logger
|
5
|
+
attr_reader :logdev
|
6
|
+
end
|
7
|
+
|
8
|
+
module LogUtils
|
9
|
+
|
10
|
+
# eg.
|
11
|
+
# {
|
12
|
+
# 'destination' => 'STDERR|STDOUT|FILE',
|
13
|
+
# 'filename' => '/path/to/file.ext',
|
14
|
+
# 'level' => 'DEBUG|INFO|...',
|
15
|
+
#
|
16
|
+
# 'age' = 'daily|weekly|monthly',
|
17
|
+
# OR
|
18
|
+
# 'max_files' => 3,
|
19
|
+
# 'max_bytes' => 1024000
|
20
|
+
# }
|
21
|
+
def self.create_logger_from_config(aConfigHash)
|
22
|
+
if not aConfigHash
|
23
|
+
result = Logger.new(STDERR)
|
24
|
+
result.level = Logger::INFO
|
25
|
+
return result
|
26
|
+
end
|
27
|
+
|
28
|
+
result = nil
|
29
|
+
case aConfigHash['destination']
|
30
|
+
when 'STDERR' then
|
31
|
+
result = Logger.new(STDERR)
|
32
|
+
when 'STDOUT' then
|
33
|
+
result = Logger.new(STDOUT)
|
34
|
+
when 'FILE' then
|
35
|
+
result = aConfigHash['age'] ?
|
36
|
+
Logger.new(aConfigHash['filename'],aConfigHash['age']) :
|
37
|
+
Logger.new(
|
38
|
+
aConfigHash['filename'],
|
39
|
+
(aConfigHash['max_files'] || 3).to_i,
|
40
|
+
(aConfigHash['max_bytes'] || 1024000).to_i
|
41
|
+
)
|
42
|
+
else
|
43
|
+
result = Logger.new(STDERR)
|
44
|
+
end
|
45
|
+
puts valstr = "Logger::#{(aConfigHash['level'] || 'INFO').upcase}"
|
46
|
+
result.level = eval(valstr)
|
47
|
+
return result
|
48
|
+
end
|
49
|
+
|
50
|
+
# use this to trunc a log file to 0 bytes
|
51
|
+
def self.trunc(aFilename)
|
52
|
+
f = File.open(aFilename, "w")
|
53
|
+
f.close
|
54
|
+
end
|
55
|
+
|
56
|
+
class ReportFormatter < Logger::Formatter
|
57
|
+
def call(severity, time, progname, msg)
|
58
|
+
"|%s %1s %s\n" % [(time.strftime('%H%M%S.')<<"%03d" % (time.usec/1000)),severity[0..0],msg2str(msg)]
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
class Reporter < Logger
|
63
|
+
def initialize(logdev)
|
64
|
+
super(logdev)
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
|
69
|
+
def self.create_reporter(aFilename=nil)
|
70
|
+
aFilename ||= MiscUtils::temp_file()
|
71
|
+
result = Logger.new(aFilename)
|
72
|
+
result.formatter = ReportFormatter.new
|
73
|
+
result
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
|
78
|
+
|
79
|
+
class MultiLogger < Logger
|
80
|
+
|
81
|
+
attr_reader :loggers
|
82
|
+
|
83
|
+
def initialize(aLoggers)
|
84
|
+
@loggers = aLoggers.is_a?(Array) ? aLoggers : [aLoggers]
|
85
|
+
end
|
86
|
+
|
87
|
+
def add(severity, message = nil, progname = nil, &block)
|
88
|
+
return true if !@loggers
|
89
|
+
severity ||= UNKNOWN
|
90
|
+
@loggers.each do |lr|
|
91
|
+
block_given? ? lr.add(severity,message,progname,&block) : lr.add(severity,message,progname)
|
92
|
+
end
|
93
|
+
true
|
94
|
+
end
|
95
|
+
alias log add
|
96
|
+
|
97
|
+
def <<(msg)
|
98
|
+
@loggers.each do |lr|
|
99
|
+
lr << msg
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def close
|
104
|
+
@loggers.each do |lr|
|
105
|
+
lr.close
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
110
|
+
|
111
|
+
#DEBUG D
|
112
|
+
#INFO
|
113
|
+
#WARN ?
|
114
|
+
#ERROR !
|
115
|
+
#FATAL F
|
116
|
+
#UNKNOWN U
|
117
|
+
|
118
|
+
# Logger that mostly works like a STDOUT logger, except that warnings and above get sent to STDERR instead
|
119
|
+
class ConsoleLogger < Logger
|
120
|
+
|
121
|
+
class ReportFormatter < Logger::Formatter
|
122
|
+
def call(severity, time, progname, msg)
|
123
|
+
msg2str(msg)+"\n"
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def initialize(aErrLevel = Severity::WARN)
|
128
|
+
super(STDOUT)
|
129
|
+
self.formatter = ReportFormatter.new
|
130
|
+
self.level = Severity::INFO
|
131
|
+
self << "\n"
|
132
|
+
@err_logger = Logger.new(STDERR)
|
133
|
+
@err_level = aErrLevel
|
134
|
+
@err_logger.formatter = ReportFormatter.new
|
135
|
+
end
|
136
|
+
|
137
|
+
alias_method :orig_add, :add
|
138
|
+
def add(severity, message = nil, progname = nil, &block)
|
139
|
+
if severity >= @err_level
|
140
|
+
block_given? ? @err_logger.add(severity,message,progname,&block) : @err_logger.add(severity,message,progname)
|
141
|
+
else
|
142
|
+
block_given? ? orig_add(severity,message,progname,&block) : orig_add(severity,message,progname)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
alias log add
|
146
|
+
|
147
|
+
#
|
148
|
+
# Close the logging device.
|
149
|
+
#
|
150
|
+
def close
|
151
|
+
begin
|
152
|
+
@logdev.close if @logdev
|
153
|
+
ensure
|
154
|
+
@err_logger.close
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
end
|
159
|
+
|
data/lib/ihl_ruby/misc_utils.rb
CHANGED
@@ -2,6 +2,8 @@ require 'tmpdir'
|
|
2
2
|
require 'logger'
|
3
3
|
require 'pathname'
|
4
4
|
|
5
|
+
require 'ihl_ruby/logging'
|
6
|
+
|
5
7
|
module MiscUtils
|
6
8
|
|
7
9
|
def self.logger
|
@@ -346,81 +348,6 @@ END_OF_MESSAGE
|
|
346
348
|
|
347
349
|
end
|
348
350
|
|
349
|
-
class Logger
|
350
|
-
attr_reader :logdev
|
351
|
-
end
|
352
|
-
|
353
|
-
module LogUtils
|
354
|
-
|
355
|
-
# eg.
|
356
|
-
# {
|
357
|
-
# 'destination' => 'STDERR|STDOUT|FILE',
|
358
|
-
# 'filename' => '/path/to/file.ext',
|
359
|
-
# 'level' => 'DEBUG|INFO|...',
|
360
|
-
#
|
361
|
-
# 'age' = 'daily|weekly|monthly',
|
362
|
-
# OR
|
363
|
-
# 'max_files' => 3,
|
364
|
-
# 'max_bytes' => 1024000
|
365
|
-
# }
|
366
|
-
def self.create_logger_from_config(aConfigHash)
|
367
|
-
if not aConfigHash
|
368
|
-
result = Logger.new(STDERR)
|
369
|
-
result.level = Logger::DEBUG
|
370
|
-
return result
|
371
|
-
end
|
372
|
-
|
373
|
-
result = nil
|
374
|
-
case aConfigHash['destination']
|
375
|
-
when 'STDERR' then
|
376
|
-
result = Logger.new(STDERR)
|
377
|
-
when 'STDOUT' then
|
378
|
-
result = Logger.new(STDOUT)
|
379
|
-
when 'FILE' then
|
380
|
-
result = aConfigHash['age'] ?
|
381
|
-
Logger.new(aConfigHash['filename'],aConfigHash['age']) :
|
382
|
-
Logger.new(
|
383
|
-
aConfigHash['filename'],
|
384
|
-
(aConfigHash['max_files'] || 3).to_i,
|
385
|
-
(aConfigHash['max_bytes'] || 1024000).to_i
|
386
|
-
)
|
387
|
-
else
|
388
|
-
result = Logger.new(STDERR)
|
389
|
-
end
|
390
|
-
puts valstr = "Logger::#{(aConfigHash['level'] || 'INFO').upcase}"
|
391
|
-
result.level = eval(valstr)
|
392
|
-
return result
|
393
|
-
end
|
394
|
-
|
395
|
-
# use this to trunc a log file to 0 bytes
|
396
|
-
def self.trunc(aFilename)
|
397
|
-
f = File.open(aFilename, "w")
|
398
|
-
f.close
|
399
|
-
end
|
400
|
-
|
401
|
-
class ReportFormatter < Logger::Formatter
|
402
|
-
def call(severity, time, progname, msg)
|
403
|
-
"|%s %1s %s\n" % [(time.strftime('%H%M%S.')<<"%03d" % (time.usec/1000)),severity[0..0],msg2str(msg)]
|
404
|
-
end
|
405
|
-
end
|
406
|
-
|
407
|
-
class Reporter < Logger
|
408
|
-
def initialize(logdev)
|
409
|
-
super(logdev)
|
410
|
-
end
|
411
|
-
|
412
|
-
end
|
413
|
-
|
414
|
-
def self.create_reporter(aFilename=nil)
|
415
|
-
aFilename ||= MiscUtils::temp_file()
|
416
|
-
result = Logger.new(aFilename)
|
417
|
-
result.formatter = ReportFormatter.new
|
418
|
-
result
|
419
|
-
end
|
420
|
-
end
|
421
|
-
|
422
|
-
|
423
|
-
|
424
351
|
# include this at the top of a class to protect it from baddies.
|
425
352
|
# eg.
|
426
353
|
# + nearly all ancestor public_instance_methods will be hidden
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
gem 'RequirePaths'; require 'require_paths'
|
3
|
+
require_paths '.','..'
|
4
|
+
|
5
|
+
gem 'Platform'; require 'platform'
|
6
|
+
gem 'shairontoledo-popen4'; require 'popen4'
|
7
|
+
|
8
|
+
module POpen4
|
9
|
+
|
10
|
+
class ExecuteError < StandardError
|
11
|
+
|
12
|
+
attr_reader :result #,:stderr,:stdout,:exitcode,:pid
|
13
|
+
|
14
|
+
def initialize(aArg)
|
15
|
+
if aArg.is_a? Hash
|
16
|
+
msg = ([aArg[:stderr],aArg[:stdout],"Error #{aArg[:exitcode].to_s}"].find {|i| i && !i.empty?})
|
17
|
+
super(msg)
|
18
|
+
@result = aArg
|
19
|
+
else
|
20
|
+
super(aArg)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def inspect
|
25
|
+
"#{self.class.to_s}: #{@result.inspect}"
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.pump_thread(aIn,aOut)
|
31
|
+
Thread.new do
|
32
|
+
loop { aOut.puts aIn.gets }
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Usage :
|
37
|
+
# result = POpen4::shell('somebinary') do |r| # block gives opportunity to adjust result, and avoid exception raised from non-zero exit codes
|
38
|
+
# if r[:exitcode]==254 # eg. say this binary returns 254 to mean something special but not an error
|
39
|
+
# r[:stdout] = 'some correct output'
|
40
|
+
# r[:stderr] = ''
|
41
|
+
# r[:exitcode] = 0
|
42
|
+
# end
|
43
|
+
# end
|
44
|
+
#
|
45
|
+
# OR
|
46
|
+
#
|
47
|
+
# result = POpen4::shell('somebinary');
|
48
|
+
# puts result[:stdout]
|
49
|
+
#
|
50
|
+
# Giving aStdOut,aStdErr causes the command output to be connected to the given stream, and that stream to not be given in the result hash
|
51
|
+
def self.shell(aCommand,aWorkingDir=nil,aTimeout=nil,aStdOut=nil,aStdErr=nil)
|
52
|
+
raise ExecuteError.new('aWorkingDir doesnt exist') unless !aWorkingDir || File.exists?(aWorkingDir)
|
53
|
+
orig_wd = Dir.getwd
|
54
|
+
result = {:command => aCommand, :dir => (aWorkingDir || orig_wd)}
|
55
|
+
status = nil
|
56
|
+
begin
|
57
|
+
Dir.chdir(aWorkingDir) if aWorkingDir
|
58
|
+
Timeout.timeout(aTimeout,ExecuteError) do # nil aTimeout will not time out
|
59
|
+
status = POpen4::popen4(aCommand) do |stdout, stderr, stdin, pid|
|
60
|
+
thrOut = aStdOut ? Thread.new { aStdOut.puts stdout.read } : nil
|
61
|
+
thrErr = aStdErr ? Thread.new { aStdErr.puts stderr.read } : nil
|
62
|
+
thrOut.join if thrOut
|
63
|
+
thrErr.join if thrErr
|
64
|
+
|
65
|
+
result[:stdout] = stdout.read unless aStdOut
|
66
|
+
result[:stderr] = stderr.read unless aStdErr
|
67
|
+
result[:pid] = pid
|
68
|
+
end
|
69
|
+
end
|
70
|
+
ensure
|
71
|
+
Dir.chdir(orig_wd)
|
72
|
+
end
|
73
|
+
result[:exitcode] = (status && status.exitstatus) || 1
|
74
|
+
yield(result) if block_given?
|
75
|
+
raise ExecuteError.new(result) if result[:exitcode] != 0
|
76
|
+
return result
|
77
|
+
end
|
78
|
+
|
79
|
+
def self.shell_out(aCommand,aWorkingDir=nil,aTimeout=nil,&block)
|
80
|
+
block_given? ? POpen4::shell(aCommand,aWorkingDir,aTimeout,STDOUT,STDERR,&block) : POpen4::shell(aCommand,aWorkingDir,aTimeout,STDOUT,STDERR)
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
|
data/lib/yore.orig.rb
CHANGED
data/lib/yore/yore_core.rb
CHANGED
@@ -2,78 +2,21 @@ require 'rubygems'
|
|
2
2
|
gem 'RequirePaths'; require 'require_paths'
|
3
3
|
require_paths '.','..'
|
4
4
|
|
5
|
-
gem 'Platform'; require 'platform'
|
6
|
-
gem 'shairontoledo-popen4'; require 'popen4'
|
7
5
|
|
8
6
|
require 'fileutils'
|
9
7
|
require 'net/smtp'
|
10
8
|
|
11
9
|
require 'ihl_ruby/misc_utils'
|
10
|
+
require 'ihl_ruby/logging'
|
12
11
|
require 'ihl_ruby/string_utils'
|
13
12
|
require 'ihl_ruby/xml_utils'
|
14
13
|
require 'ihl_ruby/extend_base_classes'
|
14
|
+
require 'ihl_ruby/shell_extras'
|
15
|
+
require 'ihl_ruby/config'
|
15
16
|
|
16
17
|
THIS_FILE = __FILE__
|
17
18
|
THIS_DIR = File.dirname(THIS_FILE)
|
18
19
|
|
19
|
-
module POpen4
|
20
|
-
|
21
|
-
class ExecuteError < StandardError
|
22
|
-
|
23
|
-
attr_reader :result #,:stderr,:stdout,:exitcode,:pid
|
24
|
-
|
25
|
-
def initialize(aArg)
|
26
|
-
if aArg.is_a? Hash
|
27
|
-
super(aArg[:stdout] || '')
|
28
|
-
@result = aArg
|
29
|
-
# @stderr = aArg[:stderr]
|
30
|
-
# @stdout = aArg[:stdout]
|
31
|
-
# @exitcode = aArg[:exitcode]
|
32
|
-
# @pid = aArg[:pid]
|
33
|
-
else
|
34
|
-
super(aArg)
|
35
|
-
end
|
36
|
-
end
|
37
|
-
|
38
|
-
end
|
39
|
-
|
40
|
-
# Usage :
|
41
|
-
# result = POpen4::shell('somebinary') do |r| # block gives opportunity to adjust result, and avoid exception raised from non-zero exit codes
|
42
|
-
# if r[:exitcode]==254 # eg. say this binary returns 254 to mean something special but not an error
|
43
|
-
# r[:stdout] = 'some correct output'
|
44
|
-
# r[:stderr] = ''
|
45
|
-
# r[:exitcode] = 0
|
46
|
-
# end
|
47
|
-
# end
|
48
|
-
#
|
49
|
-
# OR
|
50
|
-
#
|
51
|
-
# result = POpen4::shell('somebinary');
|
52
|
-
# puts result[:stdout]
|
53
|
-
def self.shell(aCommand,aWorkingDir=nil,aTimeout=nil)
|
54
|
-
raise ExecuteError.new('aWorkingDir doesnt exist') unless !aWorkingDir || File.exists?(aWorkingDir)
|
55
|
-
orig_wd = Dir.getwd
|
56
|
-
result = {:command => aCommand, :dir => (aWorkingDir || orig_wd)}
|
57
|
-
status = nil
|
58
|
-
begin
|
59
|
-
Dir.chdir(aWorkingDir) if aWorkingDir
|
60
|
-
Timeout.timeout(aTimeout,ExecuteError) do # nil aTimeout will not time out
|
61
|
-
status = POpen4::popen4(aCommand) do |stdout, stderr, stdin, pid|
|
62
|
-
result[:stdout] = stdout.read
|
63
|
-
result[:stderr] = stderr.read
|
64
|
-
result[:pid] = pid
|
65
|
-
end
|
66
|
-
end
|
67
|
-
ensure
|
68
|
-
Dir.chdir(orig_wd)
|
69
|
-
end
|
70
|
-
result[:exitcode] = (status && status.exitstatus) || 1
|
71
|
-
yield(result) if block_given?
|
72
|
-
raise ExecuteError.new(result) if result[:exitcode] != 0
|
73
|
-
return result
|
74
|
-
end
|
75
|
-
|
76
|
-
end
|
77
20
|
|
78
21
|
|
79
22
|
module YoreCore
|
@@ -137,88 +80,6 @@ module YoreCore
|
|
137
80
|
end
|
138
81
|
|
139
82
|
|
140
|
-
class ConfigClass < Hash
|
141
|
-
|
142
|
-
def initialize(aDefaultHash=nil)
|
143
|
-
self.merge!(aDefaultHash) if aDefaultHash
|
144
|
-
end
|
145
|
-
|
146
|
-
def set_int(aKey,aValue)
|
147
|
-
case aValue
|
148
|
-
when String then self[aKey] = aValue.to_integer(self[aKey]);
|
149
|
-
when Fixnum then self[aKey] = aValue;
|
150
|
-
when Float then self[aKey] = aValue.to_i;
|
151
|
-
end
|
152
|
-
end
|
153
|
-
|
154
|
-
def set_float(aKey,aValue)
|
155
|
-
case aValue
|
156
|
-
when String then self[aKey] = aValue.to_float(self[aKey]);
|
157
|
-
when Fixnum then self[aKey] = aValue.to_f;
|
158
|
-
when Float then self[aKey] = aValue;
|
159
|
-
end
|
160
|
-
end
|
161
|
-
|
162
|
-
def set_boolean(aKey,aValue)
|
163
|
-
case aValue
|
164
|
-
when TrueClass,FalseClass then self[aKey] = aValue;
|
165
|
-
when String then self[aKey] = (['1','yes','y','true','on'].include?(aValue.downcase))
|
166
|
-
else
|
167
|
-
default set_boolean(aKey,aValue.to_s)
|
168
|
-
end
|
169
|
-
end
|
170
|
-
|
171
|
-
def set_symbol(aKey,aValue)
|
172
|
-
case aValue
|
173
|
-
when String then self[aKey] = (aValue.to_sym rescue nil);
|
174
|
-
when Symbol then self[aKey] = aValue;
|
175
|
-
end
|
176
|
-
end
|
177
|
-
|
178
|
-
def copy_item(aHash,aKey)
|
179
|
-
case self[aKey]
|
180
|
-
when NilClass then ;
|
181
|
-
when String then self[aKey] = aHash[aKey].to_s unless aHash[aKey].nil?
|
182
|
-
when Float then set_float(aKey,aHash[aKey]);
|
183
|
-
when Fixnum then set_int(aKey,aHash[aKey]);
|
184
|
-
when TrueClass, FalseClass then set_boolean(aKey,aHash[aKey]);
|
185
|
-
when Symbol then self[aKey] = (aHash[aKey].to_sym rescue nil)
|
186
|
-
else
|
187
|
-
raise Error.new('unsupported type')
|
188
|
-
end
|
189
|
-
end
|
190
|
-
|
191
|
-
def copy_strings(aHash,*aKeys)
|
192
|
-
aKeys.each do |k|
|
193
|
-
self[k] = aHash[k].to_s unless aHash[k].nil?
|
194
|
-
end
|
195
|
-
end
|
196
|
-
|
197
|
-
def copy_ints(*aDb)
|
198
|
-
aHash = aDb.shift
|
199
|
-
aKeys = aDb
|
200
|
-
aKeys.each do |k|
|
201
|
-
set_int(k,aHash[k])
|
202
|
-
end
|
203
|
-
end
|
204
|
-
|
205
|
-
def copy_floats(aHash,*aKeys)
|
206
|
-
aKeys.each do |k|
|
207
|
-
set_float(k,aHash[k])
|
208
|
-
end
|
209
|
-
end
|
210
|
-
|
211
|
-
def copy_booleans(aHash,*aKeys)
|
212
|
-
aKeys.each do |k|
|
213
|
-
set_boolean(k,aHash[k])
|
214
|
-
end
|
215
|
-
end
|
216
|
-
|
217
|
-
def to_hash
|
218
|
-
{}.merge(self)
|
219
|
-
end
|
220
|
-
|
221
|
-
end
|
222
83
|
|
223
84
|
class Yore
|
224
85
|
|
@@ -230,7 +91,7 @@ module YoreCore
|
|
230
91
|
:crypto_key => "07692FC8656F04AE5518B80D38681E038A3C12050DF6CC97CEEC33D800D5E2FE", # apparently a string of up to 64 random hex digits
|
231
92
|
:first_hour => 4,
|
232
93
|
:prefix => 'backup',
|
233
|
-
:log_level => '
|
94
|
+
:log_level => 'INFO',
|
234
95
|
:bucket => '',
|
235
96
|
:email_report => false,
|
236
97
|
:mail_host => '',
|
@@ -254,17 +115,26 @@ module YoreCore
|
|
254
115
|
|
255
116
|
def initialize(aConfig=nil)
|
256
117
|
DEFAULT_CONFIG[:email_report] = false # fixes some bug where this was nil
|
257
|
-
|
258
|
-
end
|
118
|
+
@config = ConfigClass.new(DEFAULT_CONFIG,aConfig)
|
259
119
|
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
120
|
+
cons = ConsoleLogger.new()
|
121
|
+
cons.level = Logger::Severity.const_get(config[:log_level]) rescue Logger::Severity::INFO
|
122
|
+
|
123
|
+
report_file = MiscUtils::temp_file
|
124
|
+
@reporter = Logger.new(report_file)
|
125
|
+
@reporter.formatter = ConsoleLogger::ReportFormatter.new
|
126
|
+
@reporter.level = cons.level
|
127
|
+
|
128
|
+
@logger = MultiLogger.new([cons,@reporter])
|
129
|
+
#require 'ruby-debug'; debugger
|
130
|
+
@logger.info "Yore file and database backup tool for Amazon S3 "
|
131
|
+
@logger.info "(c) 2009 Buzzware Solutions (www.buzzware.com.au)"
|
132
|
+
@logger.info "-------------------------------------------------"
|
133
|
+
@logger.info ""
|
134
|
+
|
135
|
+
@logger.info "report file: #{report_file}"
|
136
|
+
|
137
|
+
configure(@config)
|
268
138
|
end
|
269
139
|
|
270
140
|
# read the config however its given and return a hash with values in their correct type, and either valid or nil
|
@@ -272,45 +142,47 @@ module YoreCore
|
|
272
142
|
def configure(aConfig,aCmdOptions = nil,aOptions = nil)
|
273
143
|
config_h = nil
|
274
144
|
case aConfig
|
275
|
-
when Hash then config_h = aConfig
|
145
|
+
when Hash,::ConfigClass then config_h = aConfig
|
276
146
|
when REXML::Element then config_h = XmlUtils.read_simple_items(aConfig,'/Yore/SimpleItems')
|
277
147
|
else
|
278
|
-
raise
|
148
|
+
raise StandardError.new('unsupported type')
|
279
149
|
end
|
280
150
|
config_i = {}
|
281
151
|
config_h.each{|n,v| config_i[n.to_sym] = v} if config_h
|
282
152
|
aCmdOptions.each{|k,v| config_i[k.to_sym] = v} if aCmdOptions
|
283
153
|
config_i.merge!(aOptions) if aOptions
|
284
|
-
|
154
|
+
config.read(config_i)
|
155
|
+
|
285
156
|
@keepers = Array.new
|
286
157
|
@keepers << KeepDaily.new(config[:keep_daily])
|
287
158
|
@keepers << KeepWeekly.new(config[:keep_weekly])
|
288
159
|
@keepers << KeepMonthly.new(config[:keep_monthly])
|
289
|
-
|
290
|
-
@log_file = MiscUtils::temp_file()
|
291
|
-
|
292
|
-
@logger = LogUtils::create_logger_from_config({
|
293
|
-
'destination' => 'STDOUT',
|
294
|
-
'level' => config[:log_level]
|
295
|
-
# 'destination' => 'FILE',
|
296
|
-
# 'filename' => @log_file,
|
297
|
-
# 'level' => config[:log_level]
|
298
|
-
})
|
299
|
-
report_file = MiscUtils::temp_file
|
300
|
-
logger.info "report file: #{report_file}"
|
301
|
-
@reporter = LogUtils::create_reporter(report_file)
|
160
|
+
|
302
161
|
@basepath = config_h[:basepath]
|
303
|
-
#sources = aConfig
|
304
|
-
#
|
305
|
-
#@filelist =
|
306
162
|
end
|
163
|
+
|
164
|
+
def do_action(aAction,aArgs)
|
165
|
+
logger.info "Executing command: #{aAction} ...\n"
|
166
|
+
begin
|
167
|
+
send(aAction,aArgs)
|
168
|
+
rescue Exception => e
|
169
|
+
logger.warn "#{e.class.to_s}: during #{aAction}(#{aArgs.inspect}): #{e.message}"
|
170
|
+
end
|
171
|
+
end
|
307
172
|
|
308
173
|
def shell(aCommandline,&aBlock)
|
309
|
-
|
174
|
+
#require 'ruby-debug'; debugger
|
175
|
+
logger.debug "To shell: " + aCommandline
|
310
176
|
result = block_given? ? POpen4::shell(aCommandline,nil,nil,&aBlock) : POpen4::shell(aCommandline)
|
311
|
-
|
177
|
+
logger.debug "From shell: '#{result.inspect}'"
|
312
178
|
return result[:stdout]
|
313
179
|
end
|
180
|
+
|
181
|
+
def s3shell(aCommandline)
|
182
|
+
shell(aCommandline) do |r|
|
183
|
+
r[:exitcode] = 1 if r[:stderr].length > 0
|
184
|
+
end
|
185
|
+
end
|
314
186
|
|
315
187
|
def get_log
|
316
188
|
logger.close
|
@@ -318,7 +190,7 @@ module YoreCore
|
|
318
190
|
end
|
319
191
|
|
320
192
|
def get_report
|
321
|
-
MiscUtils::string_from_file(reporter.logdev.filename)
|
193
|
+
MiscUtils::string_from_file(@reporter.logdev.filename)
|
322
194
|
end
|
323
195
|
|
324
196
|
def self.filemap_from_filelist(aFiles)
|
@@ -334,11 +206,21 @@ module YoreCore
|
|
334
206
|
|
335
207
|
end
|
336
208
|
|
209
|
+
#def self.nice_format(aNumber)
|
210
|
+
# if aNumber >= 100
|
211
|
+
# sprintf('%.0f', aNumber)
|
212
|
+
# else
|
213
|
+
# sprintf('%.3f', aNumber).sub(/\.0{1,3}$/, '')
|
214
|
+
# end
|
215
|
+
#end
|
216
|
+
|
337
217
|
# By default, GNU tar suppresses a leading slash on absolute pathnames while creating or reading a tar archive. (You can suppress this with the -p option.)
|
338
218
|
# tar : http://my.safaribooksonline.com/0596102461/I_0596102461_CHP_3_SECT_9#snippet
|
339
219
|
|
340
220
|
# get files from wherever they are into a single file
|
341
221
|
def collect(aSourceFiles,aDestFile,aParentDir=nil)
|
222
|
+
logger.info "Collecting files ..."
|
223
|
+
logger.info aSourceFiles.join("\n")
|
342
224
|
filelist = filemap = nil
|
343
225
|
if aSourceFiles.is_a?(Hash)
|
344
226
|
filelist = aSourceFiles.keys
|
@@ -354,12 +236,17 @@ module YoreCore
|
|
354
236
|
listfile
|
355
237
|
)
|
356
238
|
tarfile = MiscUtils.file_change_ext(aDestFile, 'tar')
|
239
|
+
|
357
240
|
shell("tar cv --directory=#{aParentDir} --file=#{tarfile} --files-from=#{listfile}")
|
358
241
|
shell("tar --append --directory=#{aParentDir} --file=#{tarfile} .contents")
|
242
|
+
logger.info "Compressing ..."
|
243
|
+
tarfile_size = File.size(tarfile)
|
359
244
|
shell("bzip2 #{tarfile}; mv #{tarfile}.bz2 #{aDestFile}")
|
245
|
+
logger.info "Compressed #{'%.1f' % (tarfile_size*1.0/2**10)} KB to #{'%.1f' % (File.size(aDestFile)*1.0/2**10)} KB"
|
360
246
|
end
|
361
247
|
|
362
248
|
def pack(aFileIn,aFileOut)
|
249
|
+
logger.info "Encrypting ..."
|
363
250
|
shell "openssl enc -aes-256-cbc -K #{config[:crypto_key]} -iv #{config[:crypto_iv]} -in #{aFileIn} -out #{aFileOut}"
|
364
251
|
end
|
365
252
|
|
@@ -369,18 +256,20 @@ module YoreCore
|
|
369
256
|
|
370
257
|
def ensure_bucket(aBucket=nil)
|
371
258
|
aBucket ||= config[:bucket]
|
372
|
-
|
259
|
+
logger.info "Ensuring S3 bucket #{aBucket} exists ..."
|
260
|
+
s3shell "s3cmd createbucket #{aBucket}"
|
373
261
|
end
|
374
262
|
|
375
263
|
# uploads the given file to the current bucket as its basename
|
376
264
|
def upload(aFile)
|
377
265
|
ensure_bucket()
|
378
|
-
|
266
|
+
logger.info "Uploading #{File.basename(aFile)} to S3 bucket #{config[:bucket]} ..."
|
267
|
+
s3shell "s3cmd put #{config[:bucket]}:#{File.basename(aFile)} #{aFile}"
|
379
268
|
end
|
380
269
|
|
381
270
|
# downloads the given file from the current bucket as its basename
|
382
271
|
def download(aFile)
|
383
|
-
|
272
|
+
s3shell "s3cmd get #{config[:bucket]}:#{File.basename(aFile)} #{aFile}"
|
384
273
|
end
|
385
274
|
|
386
275
|
# calculate the date (with no time component) based on :day_begins_hour and the local time
|
@@ -407,7 +296,6 @@ module YoreCore
|
|
407
296
|
#end
|
408
297
|
|
409
298
|
def backup_process(aSourceFiles,aTimeNow=Time.now,aTempDir=nil)
|
410
|
-
#require 'ruby-debug'; debugger
|
411
299
|
aTempDir ||= MiscUtils.make_temp_dir('yore_')
|
412
300
|
temp_file = File.expand_path('backup.tar',aTempDir)
|
413
301
|
collect(aSourceFiles,temp_file)
|
@@ -418,6 +306,7 @@ module YoreCore
|
|
418
306
|
|
419
307
|
# aDb : Hash containing :db_host,db_user,db_password,db_name,
|
420
308
|
def db_to_file(aDb,aFile)
|
309
|
+
logger.info "Dumping database #{aDb[:db_name]} ..."
|
421
310
|
shell "#{config[:mysqldump]} --host=#{aDb[:db_host]} --user=#{aDb[:db_user]} --password=#{aDb[:db_password]} --databases --skip-extended-insert --add-drop-database #{aDb[:db_name]} > #{aFile}"
|
422
311
|
end
|
423
312
|
|
@@ -449,6 +338,7 @@ module YoreCore
|
|
449
338
|
def report
|
450
339
|
return unless config[:email_report]
|
451
340
|
msg = get_report()
|
341
|
+
logger.info "Sending report via email to #{config[:mail_to]} ..."
|
452
342
|
MiscUtils::send_email(
|
453
343
|
:host => config[:mail_host],
|
454
344
|
:port => config[:mail_port],
|
@@ -476,7 +366,6 @@ module YoreCore
|
|
476
366
|
end
|
477
367
|
|
478
368
|
def backup(aJobFiles)
|
479
|
-
#require 'ruby-debug'; debugger
|
480
369
|
return unless job = aJobFiles.is_a?(Array) ? aJobFiles.first : aJobFiles # just use first job
|
481
370
|
|
482
371
|
xmlRoot = XmlUtils.get_file_root(job)
|
@@ -501,16 +390,16 @@ module YoreCore
|
|
501
390
|
args = Yore::database_from_xml(xmlDb)
|
502
391
|
file = args.delete(:file)
|
503
392
|
unless args[:db_host] && args[:db_user] && args[:db_password] && args[:db_name] && file
|
504
|
-
raise
|
393
|
+
raise StandardError.new("Invalid or missing parameter")
|
505
394
|
end
|
506
395
|
db_to_file(args,file)
|
507
|
-
filelist
|
396
|
+
filelist << file
|
508
397
|
sourceFound = true
|
509
398
|
end
|
510
399
|
end
|
511
400
|
end
|
512
401
|
|
513
|
-
raise
|
402
|
+
raise StandardError.new("Backup source found but file list empty") if sourceFound && filelist.empty?
|
514
403
|
|
515
404
|
filelist.uniq!
|
516
405
|
filelist.sort!
|
@@ -520,7 +409,6 @@ module YoreCore
|
|
520
409
|
|
521
410
|
backup_process(filelist,time,tempdir)
|
522
411
|
#clean
|
523
|
-
report
|
524
412
|
end
|
525
413
|
|
526
414
|
def test_email(*aDb)
|
@@ -553,11 +441,11 @@ module YoreCore
|
|
553
441
|
else
|
554
442
|
xmlDb = XmlUtils::single_node(xmlRoot,"/Yore/Sources/Source[@Type='MySql']/Database")
|
555
443
|
end
|
556
|
-
raise
|
444
|
+
raise StandardError.new("No database") unless xmlDb
|
557
445
|
args = Yore.database_from_xml(xmlDb)
|
558
446
|
file = args.delete(:file)
|
559
447
|
unless args[:db_host] && args[:db_user] && args[:db_password] && args[:db_name] && file
|
560
|
-
raise
|
448
|
+
raise StandardError.new("Invalid or missing parameter")
|
561
449
|
end
|
562
450
|
db_to_file(args,file)
|
563
451
|
end
|
data/test/test_job_a.xml
CHANGED
@@ -8,7 +8,7 @@
|
|
8
8
|
<Item Name="crypto_key">07692FC8656F04AE5518B80D38681E038A3C12050DF6CC97CEEC33D800D5E2FE</Item>
|
9
9
|
<Item Name="first_hour">4</Item>
|
10
10
|
<Item Name="prefix">backup</Item>
|
11
|
-
<Item Name="log_level">
|
11
|
+
<Item Name="log_level">INFO</Item>
|
12
12
|
</SimpleItems>
|
13
13
|
<Sources>
|
14
14
|
<Source>
|
data/test/test_job_b.xml
CHANGED
@@ -9,7 +9,7 @@
|
|
9
9
|
<Item Name="bucket">yoretest</Item>
|
10
10
|
<Item Name="first_hour">4</Item>
|
11
11
|
<Item Name="prefix">yoretest</Item>
|
12
|
-
<Item Name="log_level">
|
12
|
+
<Item Name="log_level">INFO</Item>
|
13
13
|
<Item Name="email_report">false</Item>
|
14
14
|
|
15
15
|
<Item Name="mail_host">mail.authsmtp.com</Item>
|
data/test/yore_test.rb
CHANGED
@@ -7,6 +7,7 @@ gem 'Shoulda'; require 'shoulda'
|
|
7
7
|
|
8
8
|
|
9
9
|
require 'ihl_ruby/misc_utils'
|
10
|
+
require 'ihl_ruby/shell_extras'
|
10
11
|
|
11
12
|
require 'fileutils'
|
12
13
|
|
@@ -126,7 +127,7 @@ job_template = <<EOF
|
|
126
127
|
<Item Name="crypto_iv">3A63775C1E3F291B0925578165EB917E</Item>
|
127
128
|
<Item Name="crypto_key">07692FC8656F04AE5518B80D38681E038A3C12050DF6CC97CEEC33D800D5E2FE</Item>
|
128
129
|
<Item Name="prefix">backup</Item>
|
129
|
-
<Item Name="log_level">
|
130
|
+
<Item Name="log_level">INFO</Item>
|
130
131
|
<Item Name="bucket">${BUCKET}</Item>
|
131
132
|
</SimpleItems>
|
132
133
|
<Sources>
|
@@ -186,6 +187,14 @@ EOF
|
|
186
187
|
aController = YoreCore::Yore.new # main program object
|
187
188
|
job = File.expand_path('../../test/test_job_b.xml',THIS_DIR)
|
188
189
|
xmlRoot = XmlUtils.get_file_root(job)
|
190
|
+
srcdir = '/tmp/yoretest'
|
191
|
+
FileUtils.rm_rf srcdir+'/*'
|
192
|
+
FileUtils.mkdir_p srcdir
|
193
|
+
['a','a/1','b','c'].each {|p| FileUtils.mkdir_p(File.expand_path(p,srcdir))}
|
194
|
+
%w(a/blah.txt a/1/blahblah.txt b/apples.txt c/carrots.txt).each do |f|
|
195
|
+
MiscUtils::make_temp_file(f,srcdir)
|
196
|
+
end
|
197
|
+
|
189
198
|
aController.configure(xmlRoot,nil,{:basepath => File.dirname(File.expand_path(job))})
|
190
199
|
aController.backup(job)
|
191
200
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: yore
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Gary McGhee
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2009-02-
|
12
|
+
date: 2009-02-20 00:00:00 +09:00
|
13
13
|
default_executable:
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
@@ -90,8 +90,11 @@ files:
|
|
90
90
|
- lib/ihl_ruby/enum.rb
|
91
91
|
- lib/ihl_ruby/extend_base_classes.rb
|
92
92
|
- lib/ihl_ruby/misc_utils.rb
|
93
|
+
- lib/ihl_ruby/config.rb
|
94
|
+
- lib/ihl_ruby/logging.rb
|
93
95
|
- lib/ihl_ruby/string_utils.rb
|
94
96
|
- lib/ihl_ruby/xml_utils.rb
|
97
|
+
- lib/ihl_ruby/shell_extras.rb
|
95
98
|
- lib/yore.orig.rb
|
96
99
|
- lib/yore/yore_core.rb
|
97
100
|
- Manifest.txt
|