ubsafe 0.5

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