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