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 +9 -0
- data/bin/ubsafe +2 -0
- data/bin/ubsafe_cmd_with_password.expect +11 -0
- data/bin/ubsafe_file_exists +9 -0
- data/bin/ubsafe_file_mtime +10 -0
- data/bin/ubsafe_scp_cmd.expect +12 -0
- data/bin/ubsafe_ssh_cmd.expect +11 -0
- data/bin/ubsafer +22 -0
- data/lib/ubsafe.rb +21 -0
- data/lib/ubsafe/extensions/ubsafe_extensions.rb +5 -0
- data/lib/ubsafe/extensions/ubsafe_file_extensions.rb +34 -0
- data/lib/ubsafe/extensions/ubsafe_hash_extensions.rb +15 -0
- data/lib/ubsafe/extensions/ubsafe_integer_extensions.rb +45 -0
- data/lib/ubsafe/extensions/ubsafe_kernel_extensions.rb +35 -0
- data/lib/ubsafe/extensions/ubsafe_logging_extensions.rb +136 -0
- data/lib/ubsafe/ubsafe_commands/ubsafe_command_backup.rb +647 -0
- data/lib/ubsafe/ubsafe_commands/ubsafe_command_backup_mysql.rb +54 -0
- data/lib/ubsafe/ubsafe_commands/ubsafe_command_backup_postgres.rb +54 -0
- data/lib/ubsafe/ubsafe_commands/ubsafe_command_backup_svn.rb +53 -0
- data/lib/ubsafe/ubsafe_commands/ubsafe_commands.rb +12 -0
- data/lib/ubsafe/ubsafe_config.rb +166 -0
- metadata +110 -0
data/README
ADDED
data/bin/ubsafe
ADDED
@@ -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
|
+
|
data/bin/ubsafer
ADDED
@@ -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
|
+
|
data/lib/ubsafe.rb
ADDED
@@ -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,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
|