ubsafe 0.5

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/README ADDED
@@ -0,0 +1,9 @@
1
+ README
2
+ ========================================================================
3
+ ubsafe - simplify and automate backup tasks
4
+
5
+ Installation
6
+ ------------
7
+
8
+ (Soon) gem install ubsafe
9
+
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env ruby
2
+ require 'ubsafe'
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/expect
2
+
3
+ set cmd [lindex $argv 0]
4
+ set params [lindex $argv 2]
5
+ set password [lindex $argv 1]
6
+ spawn $cmd $params
7
+ expect "Password:"
8
+ send "$password\r"
9
+ expect eof
10
+
11
+
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ if File.exists?(ARGV[0])
4
+ puts "0"
5
+ exit 0
6
+ else
7
+ puts "1"
8
+ exit 1
9
+ end
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ file_name = ARGV[0]
4
+
5
+ if File.exists?(file_name)
6
+ original_file_mtime_utc = File.mtime(file_name).getutc
7
+ puts original_file_mtime_utc.strftime('%Y-%m-%d %H:%M:%S')
8
+ else
9
+ puts ''
10
+ end
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/expect
2
+
3
+ set user_and_host [lindex $argv 0]
4
+ set password [lindex $argv 1]
5
+ set source_file [lindex $argv 2]
6
+ set destination [lindex $argv 3]
7
+ spawn scp $source_file $user_and_host:$destination
8
+ expect "Password:"
9
+ send "$password\r"
10
+ expect eof
11
+
12
+
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/expect
2
+
3
+ set user_and_host [lindex $argv 0]
4
+ set password [lindex $argv 1]
5
+ set ssh_command [lindex $argv 2]
6
+ spawn ssh $user_and_host "$ssh_command"
7
+ expect "Password:"
8
+ send "$password\r"
9
+ expect eof
10
+
11
+
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+
5
+ # If we're in development mode - i.e running in the root of the gem source
6
+ script_dir = File.dirname(File.expand_path(__FILE__))
7
+ current_dir = Dir.getwd
8
+ if script_dir.index(current_dir)
9
+ unless defined?(::UBSAFE_ROOT)
10
+ puts "ubsafe: WARNING operating in development mode"
11
+ UBSAFE_ROOT = current_dir
12
+ require File.expand_path(File.join(current_dir, 'lib', 'ubsafe','extensions', 'ubsafe_extensions'))
13
+ require File.join_from_here('ubsafe','ubsafe_config')
14
+ require File.join_from_here('ubsafe','ubsafe_commands','ubsafe_commands')
15
+ end
16
+ else
17
+ gem 'ubsafe','0.5'
18
+ require 'ubsafe'
19
+ end
20
+
21
+ UBSafe::Commands::Backup.instance(ARGV).backup
22
+
@@ -0,0 +1,21 @@
1
+ module UBSafe
2
+
3
+ end
4
+
5
+ require 'rubygems'
6
+ gem 'fastthread','= 1.0.7'
7
+ require 'fastthread'
8
+ gem 'logging','= 0.9.4'
9
+ require 'logging'
10
+
11
+ unless defined?(::UBSAFE_ROOT)
12
+
13
+ UBSAFE_ROOT = File.expand_path(File.join(File.dirname(__FILE__),'..'))
14
+
15
+ require File.expand_path(File.join(File.dirname(__FILE__), 'ubsafe','extensions', 'ubsafe_extensions'))
16
+ require File.join_from_here('ubsafe','ubsafe_config')
17
+ require File.join_from_here('ubsafe','ubsafe_commands','ubsafe_commands')
18
+
19
+ end
20
+
21
+
@@ -0,0 +1,5 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), 'ubsafe_kernel_extensions'))
2
+ require File.expand_path(File.join(File.dirname(__FILE__), 'ubsafe_file_extensions'))
3
+ require File.join_from_here('ubsafe_integer_extensions')
4
+ require File.join_from_here('ubsafe_hash_extensions')
5
+ require File.join_from_here('ubsafe_logging_extensions')
@@ -0,0 +1,34 @@
1
+ class File
2
+
3
+ alias_class_method :join
4
+
5
+ class << self
6
+
7
+ # With thanks to the Mack framework
8
+
9
+ ##
10
+ # Join a list of paths as strings and/or arrays of strings
11
+ #
12
+ # @param args [Array] (Nested) list of paths to join
13
+ # @return Joined list
14
+ # Join now works like it should! It calls .to_s on each of the args
15
+ # pass in. It handles nested Arrays, etc...
16
+ #
17
+ def join(*args)
18
+ fs = [args].flatten
19
+ _ubsafe_original_join(fs.collect{|c| c.to_s})
20
+ end
21
+
22
+ ##
23
+ # Perform a join relative to the current file. Fully qualify the path name.
24
+ #
25
+ # @param args [Array] (Nested) list of paths to join
26
+ #
27
+ def join_from_here(*args)
28
+ caller.first.match(/(.+):.+/)
29
+ File.expand_path(File.expand_path(File.join(File.dirname($1), *args)))
30
+ end
31
+
32
+ end
33
+
34
+ end
@@ -0,0 +1,15 @@
1
+ class Hash
2
+
3
+ def dup_contents_1_level
4
+ dup = Hash.new
5
+ self.each do |key,val|
6
+ begin
7
+ dup[key] = val.dup
8
+ rescue Exception => ex
9
+ dup[key] = val
10
+ end
11
+ end
12
+ return dup
13
+ end
14
+
15
+ end
@@ -0,0 +1,45 @@
1
+ class Integer
2
+
3
+ def seconds
4
+ return self.to_i
5
+ end
6
+
7
+ alias_method :second, :seconds
8
+
9
+ def mins
10
+ return self.to_i * 60
11
+ end
12
+
13
+ alias_method :min, :mins
14
+
15
+ def hours
16
+ return self.mins * 60
17
+ end
18
+
19
+ alias_method :hour, :hours
20
+
21
+ def days
22
+ return self.hours * 24
23
+ end
24
+
25
+ alias_method :day, :days
26
+
27
+ def weeks
28
+ return self.days * 7
29
+ end
30
+
31
+ alias_method :week, :weeks
32
+
33
+ def months
34
+ return self.days * 30
35
+ end
36
+
37
+ alias_method :month, :months
38
+
39
+ def years
40
+ return self.days * 365
41
+ end
42
+
43
+ alias_method :year, :years
44
+
45
+ end
@@ -0,0 +1,35 @@
1
+ module Kernel
2
+
3
+ # With thanks to the Mack framework
4
+
5
+ ##
6
+ # Aliases a class method to a new name. It will only do the aliasing once, to prevent
7
+ # issues with reloading a class and causing a StackLevel too deep error.
8
+ # The method takes two arguments, the first is the original name of the method, the second,
9
+ # optional, parameter is the new name of the method. If you don't specify a new method name
10
+ # it will be generated with _original_<original_name>.
11
+ #
12
+ # Example:
13
+ # class President
14
+ # alias_class_method :good
15
+ # alias_class_method :bad, :old_bad
16
+ # def self.good
17
+ # 'Bill ' + _original_good
18
+ # end
19
+ # def self.bad
20
+ # "Either #{old_bad}"
21
+ # end
22
+ # end
23
+ #
24
+ # @param orig_name [String] The original class method to alias
25
+ # @param new_name [String] The new alias. Defaults to '_ubsafe_original_[name]'
26
+ #
27
+ def alias_class_method(orig_name, new_name = "_ubsafe_original_#{orig_name}")
28
+ eval(%{
29
+ class << self
30
+ alias_method :#{new_name}, :#{orig_name} unless method_defined?("#{new_name}")
31
+ end
32
+ })
33
+ end
34
+
35
+ end
@@ -0,0 +1,136 @@
1
+ module Logging
2
+ module Layouts
3
+ # Layout to be shared among all StandardizedLogger logging classes
4
+ class UBSafeLoggerLayout < ::Logging::Layout
5
+
6
+ attr_reader :app_name
7
+
8
+ def initialize(configuration)
9
+ @configuration = configuration
10
+ @app_name = configuration[:log_identifier]
11
+ end
12
+
13
+ # Get the (cached) hostname for this machine
14
+ def hostname
15
+ unless defined?(@@hostname)
16
+ @@hostname = `hostname`.chomp.strip.downcase
17
+ end
18
+ return @@hostname
19
+ end
20
+
21
+ # call-seq:
22
+ # format( event )
23
+ #
24
+ # Returns a string representation of the given loggging _event_. See the
25
+ #
26
+ def format( event )
27
+ msg = format_obj(event.data)
28
+ severity_text = ::Logging::LNAMES[event.level]
29
+ preamble = "#{Time.now.utc.strftime("%Y-%m-%d %H:%M:%S")}\tUTC\t[#{severity_text}]\t[#{hostname}]\t[#{app_name}]\t[#{$$}]\t"
30
+ full_message = preamble + (msg || '[nil]')
31
+ full_message.gsub!(/\n/,' ')
32
+ full_message += "\n" unless full_message =~ /\n$/
33
+ return full_message
34
+ end
35
+
36
+ end
37
+
38
+ end
39
+ end
40
+
41
+ # Patch below changes all log-related times to UTC
42
+
43
+ require 'lockfile'
44
+
45
+ module Logging::Appenders
46
+ class RollingFile < ::Logging::Appenders::IO
47
+
48
+
49
+ def initialize( name, opts = {} )
50
+ # raise an error if a filename was not given
51
+ @fn = opts.getopt(:filename, name)
52
+ raise ArgumentError, 'no filename was given' if @fn.nil?
53
+ ::Logging::Appenders::File.assert_valid_logfile(@fn)
54
+
55
+ # grab the information we need to properly roll files
56
+ ext = ::File.extname(@fn)
57
+ bn = ::File.join(::File.dirname(@fn), ::File.basename(@fn, ext))
58
+ @rgxp = %r/\.(\d+)#{Regexp.escape(ext)}\z/
59
+ @glob = "#{bn}.*#{ext}"
60
+ @logname_fmt = "#{bn}.%d#{ext}"
61
+
62
+ # grab our options
63
+ @keep = opts.getopt(:keep, :as => Integer)
64
+ @size = opts.getopt(:size, :as => Integer)
65
+
66
+ @lockfile = if opts.getopt(:safe, false) and !::Logging::WIN32
67
+ ::Lockfile.new(
68
+ @fn + '.lck',
69
+ :retries => 1,
70
+ :timeout => 2
71
+ )
72
+ end
73
+
74
+ code = 'def sufficiently_aged?() false end'
75
+ @age_fn = @fn + '.age'
76
+
77
+ case @age = opts.getopt(:age)
78
+ when 'daily'
79
+ FileUtils.touch(@age_fn) unless test(?f, @age_fn)
80
+ code = <<-CODE
81
+ def sufficiently_aged?
82
+ now = Time.now.utc
83
+ start = ::File.mtime(@age_fn).utc
84
+ if (now.day != start.day) or (now - start) > 86400
85
+ return true
86
+ end
87
+ false
88
+ end
89
+ CODE
90
+ when 'weekly'
91
+ FileUtils.touch(@age_fn) unless test(?f, @age_fn)
92
+ code = <<-CODE
93
+ def sufficiently_aged?
94
+ if (Time.now.utc - ::File.mtime(@age_fn).utc) > 604800
95
+ return true
96
+ end
97
+ false
98
+ end
99
+ CODE
100
+ when 'monthly'
101
+ FileUtils.touch(@age_fn) unless test(?f, @age_fn)
102
+ code = <<-CODE
103
+ def sufficiently_aged?
104
+ now = Time.now.utc
105
+ start = ::File.mtime(@age_fn).utc
106
+ if (now.month != start.month) or (now - start) > 2678400
107
+ return true
108
+ end
109
+ false
110
+ end
111
+ CODE
112
+ when Integer, String
113
+ @age = Integer(@age)
114
+ FileUtils.touch(@age_fn) unless test(?f, @age_fn)
115
+ code = <<-CODE
116
+ def sufficiently_aged?
117
+ if (Time.now.utc - ::File.mtime(@age_fn).utc) > @age
118
+ return true
119
+ end
120
+ false
121
+ end
122
+ CODE
123
+ end
124
+ meta = class << self; self end
125
+ meta.class_eval code, __FILE__, __LINE__
126
+
127
+ # if the truncate flag was set to true, then roll
128
+ roll_now = opts.getopt(:truncate, false)
129
+ roll_files if roll_now
130
+
131
+ super(name, open_logfile, opts)
132
+ end
133
+
134
+ end
135
+ end
136
+
@@ -0,0 +1,647 @@
1
+ require 'parsedate'
2
+ require 'net/smtp'
3
+ require 'tmpdir'
4
+
5
+ module UBSafe
6
+
7
+ module Commands
8
+
9
+ class Backup
10
+
11
+ class << self
12
+
13
+ ##
14
+ # Get backup-specific backup instance
15
+ #
16
+ # @param [Array] args Command-line arguments
17
+ #
18
+ def instance(args)
19
+ @config = UBSafe::Config.config
20
+ @config.load(args)
21
+ @backup_name = @config.options[:backup_name]
22
+ @backup_options = @config.full_options(@backup_name)
23
+ backup_class = @backup_options[:backup_class]
24
+ backup_instance = nil
25
+ if backup_class == :default
26
+ backup_instance = UBSafe::Commands::Backup.new(args)
27
+ else
28
+ eval("backup_instance = #{backup_class.to_s}.new(args)")
29
+ end
30
+ return backup_instance
31
+ end
32
+
33
+ end
34
+
35
+ ##
36
+ # Create a new backup instance
37
+ #
38
+ # @param [Array] args Command-line arguments
39
+ #
40
+ def initialize(args)
41
+ @config = UBSafe::Config.config
42
+ @config.load(args)
43
+ @backup_name = @config.options[:backup_name]
44
+ @backup_options = @config.full_options(@backup_name)
45
+ @log = @config.log
46
+ end
47
+
48
+ ##
49
+ # Perform backup
50
+ #
51
+ # @return [Integer] Exit code 0 success, 1 otherwise
52
+ #
53
+ def backup
54
+ # Create backup file in source tree
55
+ # Rotate destination files
56
+ # Copy to destination
57
+ # Remove from source tree
58
+ backup_steps = [
59
+ :before_rotate_destination_files, :rotate_destination_files, :after_rotate_destination_files,
60
+ :before_source_backup,:create_source_backup,:after_source_backup,
61
+ :before_copy_backup, :copy_backup, :after_copy_backup,
62
+ :before_clean_source, :clean_source, :after_clean_source
63
+ ]
64
+ backup_steps.each do |backup_step|
65
+ status = self.send(backup_step)
66
+ if status == :failure
67
+ @log.error("Backup #{@backup_name} backup step #{backup_step.to_s} failed.")
68
+ email_notify(:failure,backup_step)
69
+ return 1
70
+ end
71
+ end
72
+ @log.info("Backup #{@backup_name} succeeded")
73
+ email_notify(:success,nil)
74
+ return 0
75
+ end
76
+
77
+ ##
78
+ # Hook to allow customization before creating source backup
79
+ #
80
+ # @return [Symbol] :success or :failure
81
+ #
82
+ def before_source_backup
83
+ return :success
84
+ end
85
+
86
+ ##
87
+ # Hook to allow customization after creating source backup
88
+ #
89
+ # @return [Symbol] :success or :failure
90
+ #
91
+ def after_source_backup
92
+ return :success
93
+ end
94
+
95
+ ##
96
+ # Create source backup
97
+ #
98
+ # @param [Hash] backup_options
99
+ # @param [String] backup_name
100
+ # @return [Symbol] :success or :failure
101
+ #
102
+ def create_source_backup(backup_options = nil,backup_name = nil)
103
+
104
+ backup_options ||= @backup_options
105
+ backup_name ||= @backup_name
106
+
107
+ # Don't bother to create the backup if it already exists on the destination - i.e. hasn't been rolled
108
+ return :success if remote_file_exists?
109
+
110
+ # Fully qualify directories
111
+ source_tree = File.expand_path(backup_options[:source_tree])
112
+ tmp_dir = File.expand_path(backup_options[:temporary_directory])
113
+
114
+ if backup_options[:backup_style] == :tar_gz
115
+ status = nil
116
+ # Run command somewhere sensible
117
+ FileUtils.mkdir_p(tmp_dir)
118
+ Dir.chdir(tmp_dir) do |dir|
119
+ backup_cmd_tempate = "tar cfz %s -C %s ."
120
+ full_cmd = sprintf(backup_cmd_tempate,get_backup_file_name,source_tree)
121
+ @log.debug("create_source_backup #{full_cmd}")
122
+ cmd_result = `#{full_cmd}`
123
+ cmd_status = $?
124
+ status = cmd_status == 0 ? :success : :failure
125
+ if status == :success
126
+ @log.info("Backup '#{backup_name}' succeeded")
127
+ else
128
+ @log.error("Backup '#{backup_name}' failed")
129
+ end
130
+ end
131
+ return status
132
+ end
133
+ @log.error("Backup '#{backup_name}' - backup type specified is not supported")
134
+ return :failure
135
+ end
136
+
137
+ ##
138
+ # Hook to allow customization before rotating destination files
139
+ #
140
+ # @return [Symbol] :success or :failure
141
+ #
142
+ def before_rotate_destination_files
143
+ return :success
144
+ end
145
+
146
+ ##
147
+ # Hook to allow customization after rotating destination files
148
+ #
149
+ # @return [Symbol] :success or :failure
150
+ #
151
+ def after_rotate_destination_files
152
+ return :success
153
+ end
154
+
155
+ ##
156
+ # Rotate destination files. Check all the conditions
157
+ #
158
+ # @param [Hash] backup_options
159
+ # @param [String] backup_name
160
+ # @return [Symbol] :success or :failure
161
+ #
162
+ def rotate_destination_files(backup_options = nil,backup_name = nil)
163
+ backup_options ||= @backup_options
164
+ backup_name ||= @backup_name
165
+ begin
166
+ # Make sure remote directory exists
167
+ remote_directory = File.join(backup_options[:base_backup_directory],backup_name)
168
+ remote_cmd = "mkdir -p #{remote_directory}"
169
+ cmd_status, cmd_output = ssh_cmd(remote_cmd)
170
+ return :failure unless cmd_status == :success
171
+
172
+ backup_file_name = get_backup_file_name(backup_options)
173
+ remote_file_name = File.join(remote_directory,backup_file_name)
174
+ remote_file_mtime = get_remote_modified_timestamp(remote_file_name,backup_options,backup_name)
175
+ return_status = nil
176
+
177
+ backup_frequency = backup_options[:backup_frequency]
178
+ case backup_frequency
179
+ when Integer
180
+ # Explicit age
181
+ if remote_file_mtime
182
+ if (Time.now.utc - remote_file_mtime ) > backup_frequency
183
+ return_status = rotate_destination_files_unconditionally(backup_options,backup_name)
184
+ end
185
+ end
186
+ when :daily
187
+ if remote_file_mtime
188
+ now = Time.now.utc
189
+ if (now.day != remote_file_mtime.day) or (now - remote_file_mtime) > 1.day
190
+ return_status = rotate_destination_files_unconditionally(backup_options,backup_name)
191
+ end
192
+ end
193
+ when :weekly
194
+ if remote_file_mtime
195
+ if (Time.now.utc - remote_file_mtime) > 1.week
196
+ return_status = rotate_destination_files_unconditionally(backup_options,backup_name)
197
+ end
198
+ end
199
+ when :monthly
200
+ if remote_file_mtime
201
+ now = Time.now.utc
202
+ if (now.month != remote_file_mtime.month) or (now - remote_file_mtime) > 1.month
203
+ return_status = rotate_destination_files_unconditionally(backup_options,backup_name)
204
+ end
205
+ end
206
+ end
207
+ rescue Exception => ex
208
+ @log.error("Error detected while determining whether to rotate files")
209
+ @log.error(ex.to_s)
210
+ @log.error(ex.backtrace.join("\n"))
211
+ return_status = :failure
212
+ end
213
+
214
+ return return_status
215
+
216
+ end
217
+
218
+ ##
219
+ # Rotate destination files unconditionally. Assume someone else has checked whether this is needed.
220
+ #
221
+ # @param [Hash] backup_options
222
+ # @param [String] backup_name
223
+ # @return [Symbol] :success or :failure
224
+ #
225
+ def rotate_destination_files_unconditionally(backup_options = nil,backup_name = nil)
226
+ # Assume that all checks have been performed before calling this method.
227
+ # This method will rotate files unconditionally
228
+
229
+ backup_options ||= @backup_options
230
+ backup_name ||= @backup_name
231
+
232
+ return :failure unless backup_options[:backup_file_name_template] =~ /\%n/
233
+
234
+ remote_directory_name = File.join(backup_options[:base_backup_directory],backup_name)
235
+ remote_cmd = "mkdir -p #{remote_directory_name}"
236
+ cmd_status, cmd_output = ssh_cmd(remote_cmd)
237
+ return :failure unless cmd_status == :success
238
+ remote_cmd = "ls -1t #{remote_directory_name}"
239
+ cmd_status, cmd_output = ssh_cmd(remote_cmd)
240
+ return :failure unless cmd_status == :success
241
+ remote_backup_files = cmd_output.reject {|line| line =~ /^\.$/ or line =~ /^\.\.$/ }
242
+ # Make sure we're initialized
243
+ backup_options[:all_possible_file_names] = all_backup_names(backup_options)
244
+ backups_to_retain = backup_options[:backups_to_retain]
245
+ # If no entries don't rotate
246
+ # If entries > 0 and entries < max, move all files down one position
247
+ # If entries >= max, remove last entry, then move all files down one position
248
+ unless remote_backup_files.empty?
249
+ # if entries >= max,
250
+ if remote_backup_files.size >= backups_to_retain
251
+ # .. remove last entry
252
+ last_entry = remote_backup_files.pop
253
+ remote_cmd = "rm -f #{File.join(remote_directory_name,last_entry)}"
254
+ cmd_status, cmd_output = ssh_cmd(remote_cmd)
255
+ return :failure unless cmd_status == :success
256
+ end
257
+ # Need to reverse order
258
+ remote_backup_files.size.downto(1) do |current_generation|
259
+ #puts "rotate_destination_files_unconditionally current_generation #{current_generation}"
260
+ file_name_current_generation = remote_backup_files[current_generation - 1]
261
+ #puts "rotate_destination_files_unconditionally file_name_current_generation #{file_name_current_generation}"
262
+ file_name_previous_generation = backup_options[:all_possible_file_names][current_generation]
263
+ #puts "rotate_destination_files_unconditionally file_name_previous_generation #{file_name_previous_generation}"
264
+ remote_cmd = "mv #{File.join(remote_directory_name,file_name_current_generation)} #{File.join(remote_directory_name,file_name_previous_generation)}"
265
+ cmd_status, cmd_output = ssh_cmd(remote_cmd)
266
+ @log.debug("Rotated #{File.join(remote_directory_name,file_name_current_generation)} to #{File.join(remote_directory_name,file_name_previous_generation)} Status #{cmd_status}")
267
+ return :failure unless cmd_status == :success
268
+ end
269
+
270
+ end
271
+
272
+ return :success
273
+
274
+ end
275
+
276
+ ##
277
+ # Hook to allow customization before copying backup
278
+ #
279
+ # @return [Symbol] :success or :failure
280
+ #
281
+ def before_copy_backup
282
+ return :success
283
+ end
284
+
285
+ ##
286
+ # Hook to allow customization after copying backup
287
+ #
288
+ # @return [Symbol] :success or :failure
289
+ #
290
+ def after_copy_backup
291
+ return :success
292
+ end
293
+
294
+ ##
295
+ # Create and copy backup
296
+ #
297
+ # @param [Hash] backup_options
298
+ # @param [String] backup_name
299
+ # @return [Symbol] :success or :failure
300
+ #
301
+ def create_and_copy_backup(backup_options = nil,backup_name = nil)
302
+
303
+ end
304
+ ##
305
+ # Copy backup
306
+ #
307
+ # @param [Hash] backup_options
308
+ # @param [String] backup_name
309
+ # @return [Symbol] :success or :failure
310
+ #
311
+ def copy_backup(backup_options = nil,backup_name = nil)
312
+ backup_options ||= @backup_options
313
+ backup_name ||= @backup_name
314
+ # Fully qualify directories
315
+ tmp_dir = File.expand_path(backup_options[:temporary_directory])
316
+ backup_file_name = get_backup_file_name(backup_options)
317
+ qualified_backup_file_name = File.join(tmp_dir,backup_file_name)
318
+ remote_directory_name = File.join(backup_options[:base_backup_directory],backup_name)
319
+ qualified_remote_file_name = File.join(remote_directory_name,backup_file_name)
320
+ unless remote_file_exists?(qualified_remote_file_name)
321
+ # Only copy file if it's not there
322
+ cmd_status, cmd_output = scp_cmd(qualified_backup_file_name,remote_directory_name,backup_options,backup_name)
323
+ return :failure unless cmd_status == :success
324
+ end
325
+ return :success
326
+ end
327
+
328
+ ##
329
+ # Hook to allow customization before cleaning source
330
+ #
331
+ # @return [Symbol] :success or :failure
332
+ #
333
+ def before_clean_source
334
+ return :success
335
+ end
336
+
337
+ ##
338
+ # Hook to allow customization after cleaning source
339
+ #
340
+ # @return [Symbol] :success or :failure
341
+ #
342
+ def after_clean_source
343
+ return :success
344
+ end
345
+
346
+ ##
347
+ # Clean Source - remove (temporary) backup files from source
348
+ #
349
+ # @param [Hash] backup_options
350
+ # @param [String] backup_name
351
+ # @return [Symbol] :success or :failure
352
+ #
353
+ def clean_source(backup_options = nil,backup_name = nil)
354
+ backup_options ||= @backup_options
355
+ backup_name ||= @backup_name
356
+ # Fully qualify directories
357
+ tmp_dir = File.expand_path(backup_options[:temporary_directory])
358
+ backup_file_name = File.join(tmp_dir,get_backup_file_name(backup_options))
359
+ cmd_output = `rm -f #{backup_file_name}`
360
+ cmd_status = $?
361
+ return cmd_status == 0 ? :success : :failure
362
+ end
363
+
364
+ ##
365
+ # Notify via email
366
+ #
367
+ # @param [Symbol] status
368
+ # @param [Symbol] processing_step
369
+ # @return [Symbol] :success or :failure
370
+ #
371
+ def email_notify(status = :failure,processing_step = nil)
372
+ mail_config = @config.options[:backup_email]
373
+ return :success unless mail_config[:enabled]
374
+ backup_options = @backup_options
375
+ backup_name = @backup_name
376
+ hostname = `hostname`.chomp.strip
377
+ log_directory = File.expand_path(@config.options[:logging][:log_directory])
378
+ if status == :success
379
+ subject = "#{mail_config[:mail_subject_prefix]} backup '#{backup_name}' OK"
380
+ body = "Backup '#{backup_name}' succeeded"
381
+ body << "\n\nLogs are available at #{hostname}:#{log_directory}"
382
+ else
383
+ processing_step = processing_step ? processing_step.to_s : 'unknown'
384
+ subject = "#{mail_config[:mail_subject_prefix]} backup '#{backup_name}' failed at backup stage '#{processing_step}'"
385
+ body = "backup '#{backup_name}' failed at backup stage '#{processing_step}'"
386
+ body << "\n\nLogs are available at #{hostname}:#{log_directory}"
387
+ end
388
+ email_date = Time.now.utc.strftime('%a, %d %b %y %H:%M:%S')
389
+ recipients = mail_config[:mail_to]
390
+ recipients.each do |recipient|
391
+
392
+ full_body = <<BODY
393
+ Mime-Version: 1.0
394
+ Content-Transfer-Encoding: 7bit
395
+ Content-Type: text/plain;
396
+ charset=US-ASCII;
397
+ format=flowed
398
+ To: #{recipient}
399
+ From: #{mail_config[:mail_from]}
400
+ Subject: #{subject}
401
+ Date: #{email_date}
402
+
403
+ #{body}
404
+
405
+ BODY
406
+ if mail_config[:mail_style] == :smtp
407
+ Net::SMTP.start(mail_config[:smtp_host]) do |session|
408
+ session.sendmail(full_body, mail_config[:mail_from] , recipient)
409
+ end
410
+ else
411
+ tmp_dir = Dir.tmpdir
412
+ mail_msg_file = File.join(tmp_dir,'mail.txt')
413
+ File.open(mail_msg_file,'w') do |file|
414
+ file.write full_body
415
+ end
416
+ cmd = "cat #{mail_msg_file} | sendmail #{recipient}"
417
+ #puts "email_notify cat_sendmail issuing command \"#{cmd}\""
418
+ cmd_output = `#{cmd}`
419
+ cmd_status = $?
420
+ File.delete(mail_msg_file)
421
+ @log.debug("email_notify: cat_sendmail status #{cmd_status} output #{cmd_output}")
422
+ end
423
+ end
424
+ return :success
425
+ end
426
+
427
+ ##
428
+ # Get backup name - use the template defined in the configuration file
429
+ #
430
+ # @param [Hash] backup_options Backup options
431
+ # @return [String] Unqualified name of backup file
432
+ #
433
+ def get_backup_file_name(backup_options = nil)
434
+ backup_options ||= @backup_options
435
+ return get_backup_file_name_with_generation(backup_options,0)
436
+ end
437
+
438
+ ##
439
+ # Get ordered list of all possible backup names given the supplied configuration
440
+ #
441
+ # @param [Hash] backup_options Backup options
442
+ # @return [Array] Ordered list of backup names - newest first
443
+ #
444
+ def all_backup_names(backup_options = nil)
445
+ backup_options ||= @backup_options
446
+ all_possible_file_names = []
447
+ total_possible_file_names = backup_options[:backups_to_retain]
448
+ total_possible_file_names.times do |current_generation|
449
+ all_possible_file_names[current_generation] = get_backup_file_name_with_generation(backup_options,current_generation)
450
+ end
451
+ @backup_options[:all_possible_file_names] = all_possible_file_names
452
+ return all_possible_file_names
453
+ end
454
+
455
+ ##
456
+ # Get backup name with the specified generation- use the template defined in the configuration file
457
+ #
458
+ # @param [Hash] backup_options Backup options
459
+ # @param [Integer] generation Generation number
460
+ # @return [String] Unqualified name of backup file
461
+ #
462
+ def get_backup_file_name_with_generation(backup_options = nil, generation = 1)
463
+ backup_options ||= @backup_options
464
+ template = backup_options[:backup_file_name_template]
465
+ file_name = backup_options[:backup_name]
466
+ # The new backup is always 1
467
+ file_number = generation
468
+ # Get default time zone
469
+ time_zone = (backup_options[:time_zone] || 'UTC').to_s.upcase
470
+ current_time = time_zone == 'UTC' ? Time.now.utc : Time.now
471
+ time_stamp = current_time.strftime(backup_options[:timestamp_format])
472
+ date_stamp = current_time.strftime(backup_options[:date_format])
473
+ # Translate the template to a sprintf format string
474
+ # %f => file name
475
+ # %n => backup_options[:number_format] for file number
476
+ # %t => backup_options[:timestamp_format]
477
+ # %d => backup_options[:date_format]
478
+ sprintf_template = template.gsub(/\%f/,'%s')
479
+ sprintf_template = sprintf_template.gsub(/\%n/,backup_options[:number_format])
480
+ sprintf_template = sprintf_template.gsub(/\%t/,'%s')
481
+ sprintf_template = sprintf_template.gsub(/\%d/,'%s')
482
+ # Now, figure out the order for the sprintf call
483
+ sprintf_fields = []
484
+ template_size = template.size
485
+ # We're matching a two-character placeholder string, so stop 2 chars from end
486
+ 0.upto(template_size - 2) do |pos|
487
+ if template[pos,2] == '%f'
488
+ sprintf_fields << file_name
489
+ elsif template[pos,2] == '%n'
490
+ sprintf_fields << file_number
491
+ elsif template[pos,2] == '%t'
492
+ sprintf_fields << time_stamp
493
+ elsif template[pos,2] == '%d'
494
+ sprintf_fields << date_stamp
495
+ end
496
+ end
497
+ backup_file_name = sprintf(sprintf_template,*sprintf_fields)
498
+ return backup_file_name
499
+ end
500
+
501
+ ##
502
+ # Get the modified time stamp for a remote file
503
+ #
504
+ # @param [String] file_name Fully qualified file name
505
+ # @param [Hash] backup_options
506
+ # @param [String] backup_name
507
+ # @return [Time] File modification time or nil if no file found remotely
508
+ #
509
+ def get_remote_modified_timestamp(file_name,backup_options = nil,backup_name = nil)
510
+ backup_options ||= @backup_options
511
+ backup_name ||= @backup_name
512
+ remote_bin_dir = backup_options[:bin_dir] ? "#{backup_options[:bin_dir]}/" : ''
513
+ remote_cmd = "#{remote_bin_dir}ubsafe_file_mtime #{file_name}"
514
+ cmd_status, cmd_output = ssh_cmd(remote_cmd,backup_options,backup_name)
515
+ file_mtime = nil
516
+ if cmd_status == :success and (not cmd_output.empty?) and (cmd_output[0] != '')
517
+ #puts "get_remote_modified_timestamp file_name #{file_name} #{cmd_output[0]}"
518
+ file_mtime = Time.utc(*ParseDate.parsedate(cmd_output[0]))
519
+ end
520
+ return file_mtime
521
+ end
522
+
523
+ ##
524
+ # Issue an ssh command
525
+ #
526
+ # @param [String] cmd Command to send
527
+ # @param [Hash] backup_options
528
+ # @param [String] backup_name
529
+ # @return [Array] [command status, command output]
530
+ #
531
+ def ssh_cmd(cmd,backup_options = nil,backup_name = nil)
532
+ backup_options ||= @backup_options
533
+ backup_name ||= @backup_name
534
+ ssh_user = backup_options[:user_name]
535
+ ssh_host = backup_options[:hostname]
536
+ ssh_password = backup_options[:password]
537
+ if ssh_password
538
+ # Need to use expect for the password if certs don't work
539
+ cmd_exe = File.expand_path(File.join(::UBSAFE_ROOT, 'bin','ubsafe_ssh_cmd.expect'))
540
+ full_cmd = "#{cmd_exe} #{ssh_user}@#{ssh_host} \"#{ssh_password}\" \"#{cmd}\""
541
+ masked_full_cmd = "#{cmd_exe} #{ssh_user}@#{ssh_host} [PASSWORD] \"#{cmd}\""
542
+ else
543
+ # Certs assumed if no password
544
+ full_cmd = "ssh #{ssh_user}@#{ssh_host} \"#{cmd}\""
545
+ masked_full_cmd = full_cmd
546
+ end
547
+ #puts "About to issue \"#{full_cmd}\""
548
+ cmd_output = `#{full_cmd}`
549
+ cmd_status = $?
550
+ @log.debug("Executed ssh status #{cmd_status} command \"#{masked_full_cmd}\"")
551
+ cmd_output_lines = cmd_output.split("\n").reject {|line| line =~ /spawn/i or line =~ /password/i }
552
+ cmd_output_cleaned = []
553
+ cmd_output_lines.each do |cmd_output_line|
554
+ cmd_output_cleaned << cmd_output_line.strip.chomp
555
+ end
556
+ cmd_status = cmd_status == 0 ? :success : :failure
557
+ return [cmd_status,cmd_output_cleaned]
558
+ end
559
+
560
+ ##
561
+ # Issue an scp command
562
+ #
563
+ # @param [String] source_file Fully qualified name of file to send
564
+ # @param [String] destination_dir Destination directory on remote host
565
+ # @param [Hash] backup_options
566
+ # @param [String] backup_name
567
+ # @return [Array] [command status, command output]
568
+ #
569
+ def scp_cmd(source_file,destination_dir,backup_options = nil,backup_name = nil)
570
+ backup_options ||= @backup_options
571
+ backup_name ||= @backup_name
572
+ ssh_user = backup_options[:user_name]
573
+ ssh_host = backup_options[:hostname]
574
+ ssh_password = backup_options[:password]
575
+ if ssh_password
576
+ # Need to use expect for the password if certs don't work
577
+ cmd_exe = File.expand_path(File.join(::UBSAFE_ROOT, 'bin','ubsafe_scp_cmd.expect'))
578
+ full_cmd = "#{cmd_exe} #{ssh_user}@#{ssh_host} \"#{ssh_password}\" #{source_file} #{destination_dir}"
579
+ masked_full_cmd = "#{cmd_exe} #{ssh_user}@#{ssh_host} [PASSWORD] #{source_file} #{destination_dir}"
580
+ else
581
+ # Certs assumed if no password
582
+ full_cmd = "scp #{source_file} #{ssh_user}@#{ssh_host}:#{destination_dir}"
583
+ masked_full_cmd = full_cmd
584
+ end
585
+ #puts "About to issue \"#{full_cmd}\""
586
+ cmd_output = `#{full_cmd}`
587
+ cmd_status = $?
588
+ @log.debug("Executed scp status #{cmd_status} command \"#{masked_full_cmd}\"")
589
+ cmd_output_lines = cmd_output.split("\n").reject {|line| line =~ /spawn/i or line =~ /password/i }
590
+ cmd_output_cleaned = []
591
+ cmd_output_lines.each do |cmd_output_line|
592
+ cmd_output_cleaned << cmd_output_line.strip.chomp
593
+ end
594
+ cmd_status = cmd_status == 0 ? :success : :failure
595
+ return [cmd_status,cmd_output_cleaned]
596
+ end
597
+
598
+ ##
599
+ # Does remote file exist?
600
+ #
601
+ # @param [Hash] backup_options
602
+ # @param [String] backup_name
603
+ # @return [Boolean] true or false
604
+ #
605
+ def remote_file_exists?(remote_file_name = nil,backup_options = nil,backup_name = nil)
606
+ backup_options ||= @backup_options
607
+ backup_name ||= @backup_name
608
+ if remote_file_name
609
+ remote_directory_name = File.dirname(remote_file_name)
610
+ else
611
+ backup_file_name = get_backup_file_name(backup_options)
612
+ remote_directory_name = File.join(backup_options[:base_backup_directory],backup_name)
613
+ remote_file_name = File.join(remote_directory_name,backup_file_name)
614
+ end
615
+ remote_bin_dir = backup_options[:bin_dir] ? "#{backup_options[:bin_dir]}/" : ''
616
+ remote_cmd = "#{remote_bin_dir}ubsafe_file_exists #{remote_file_name}"
617
+ cmd_status, cmd_output = ssh_cmd(remote_cmd)
618
+ if cmd_status == :failure or cmd_output[0].chomp.strip == '1'
619
+ return false
620
+ end
621
+
622
+ # remote_cmd = "ls #{remote_directory_name}"
623
+ # #puts "remote_file_exists? Looking for #{remote_directory_name}"
624
+ # cmd_status, cmd_output = ssh_cmd(remote_cmd)
625
+ # if cmd_status == :failure or cmd_output.join("\n") =~ /no\ssuch/i
626
+ # #puts "remote_file_exists? #{remote_directory_name} does not exist"
627
+ # return false
628
+ # else
629
+ # remote_cmd = "ls #{remote_file_name}"
630
+ # #puts "remote_file_exists? Looking for #{remote_file_name}"
631
+ # cmd_status, cmd_output = ssh_cmd(remote_cmd)
632
+ # #puts "remote_file_exists? cmd_status #{cmd_status}"
633
+ # if cmd_status == :failure or cmd_output.join("\n") =~ /no\ssuch/i
634
+ # #puts "remote_file_exists? #{remote_file_name} does not exist"
635
+ # return false
636
+ # end
637
+ # end
638
+
639
+ return true
640
+ end
641
+
642
+ end
643
+
644
+ end
645
+
646
+
647
+ end