ambethia-backup 0.0.11
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/CHANGELOG +48 -0
- data/Rakefile +26 -0
- data/TODO +7 -0
- data/bin/backup +12 -0
- data/bin/commands.sh +2 -0
- data/doc/LICENSE-GPL.txt +280 -0
- data/doc/index.html +716 -0
- data/doc/styles.css +157 -0
- data/examples/global.rb +28 -0
- data/examples/mediawiki.rb +24 -0
- data/examples/mediawiki_numeric.rb +19 -0
- data/examples/s3.rb +35 -0
- data/lib/backup.rb +24 -0
- data/lib/backup/actor.rb +208 -0
- data/lib/backup/cli.rb +144 -0
- data/lib/backup/configuration.rb +137 -0
- data/lib/backup/date_parser.rb +37 -0
- data/lib/backup/extensions.rb +17 -0
- data/lib/backup/recipes/standard.rb +113 -0
- data/lib/backup/rotator.rb +219 -0
- data/lib/backup/s3_helpers.rb +97 -0
- data/lib/backup/ssh_helpers.rb +139 -0
- data/lib/backup/state_recorder.rb +21 -0
- data/tests/actor_test.rb +70 -0
- data/tests/cleanup.sh +2 -0
- data/tests/optional/s3_test.rb +40 -0
- data/tests/rotation_test.rb +32 -0
- data/tests/s3_test.rb +40 -0
- data/tests/ssh_test.rb +21 -0
- data/tests/tests_helper.rb +5 -0
- metadata +128 -0
@@ -0,0 +1,219 @@
|
|
1
|
+
require "rake"
|
2
|
+
|
3
|
+
module Backup
|
4
|
+
class Rotator
|
5
|
+
include FileUtils
|
6
|
+
|
7
|
+
attr_reader :actor
|
8
|
+
attr_reader :configuration
|
9
|
+
alias_method :c, :configuration
|
10
|
+
|
11
|
+
def initialize(actor)
|
12
|
+
@actor = actor
|
13
|
+
@configuration = actor.configuration
|
14
|
+
end
|
15
|
+
|
16
|
+
# Take the last result and rotate it via mv on the local machine
|
17
|
+
def rotate_via_mv(last_result)
|
18
|
+
# verify that each of the directories exist, grandfathers, fathers, sons
|
19
|
+
hierarchy.each { |m| verify_local_backup_directory_exists(m) }
|
20
|
+
|
21
|
+
where = place_in
|
22
|
+
|
23
|
+
# place todays backup into the specified directory with a timestamp.
|
24
|
+
newname = timestamped_prefix(last_result)
|
25
|
+
mv last_result, "#{where}/#{newname}"
|
26
|
+
|
27
|
+
cleanup_via_mv(where, how_many_to_keep_today)
|
28
|
+
update_state
|
29
|
+
end
|
30
|
+
|
31
|
+
def update_state
|
32
|
+
return unless :numeric == c[:rotation_mode]
|
33
|
+
where = find_goes_in_by_state
|
34
|
+
|
35
|
+
previous_sons = $state.system.sons_since_last_promotion || 0
|
36
|
+
previous_fathers = $state.system.fathers_since_last_promotion || 0
|
37
|
+
|
38
|
+
case where
|
39
|
+
when "sons"
|
40
|
+
$state.system.sons_since_last_promotion = previous_sons + 1
|
41
|
+
when "fathers"
|
42
|
+
$state.system.sons_since_last_promotion = 0
|
43
|
+
$state.system.fathers_since_last_promotion = previous_fathers + 1
|
44
|
+
when "grandfathers"
|
45
|
+
$state.system.sons_since_last_promotion = 0
|
46
|
+
$state.system.fathers_since_last_promotion = 0
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def rotate_via_ssh(last_result)
|
51
|
+
ssh = Backup::SshActor.new(c)
|
52
|
+
ssh.connect
|
53
|
+
ssh.run "echo \"#{last_result}\""
|
54
|
+
|
55
|
+
hierarchy.each do |m|
|
56
|
+
dir = c[:backup_path] + "/" + m
|
57
|
+
ssh.verify_directory_exists(dir)
|
58
|
+
end
|
59
|
+
|
60
|
+
where = place_in
|
61
|
+
|
62
|
+
newname = timestamped_prefix(last_result)
|
63
|
+
ssh.run "mv #{last_result} #{where}/#{newname}"
|
64
|
+
|
65
|
+
ssh.cleanup_directory(where, how_many_to_keep_today)
|
66
|
+
ssh.close
|
67
|
+
end
|
68
|
+
|
69
|
+
# TODO
|
70
|
+
def rotate_via_ftp(last_result)
|
71
|
+
# ftp = Backup::FtpActor.new(c)
|
72
|
+
# ftp.connect
|
73
|
+
#
|
74
|
+
# hierarchy.each do |m|
|
75
|
+
# dir = c[:backup_path] + "/" + m
|
76
|
+
# ftp.verify_directory_exists(dir)
|
77
|
+
# end
|
78
|
+
#
|
79
|
+
# newname = timestamped_prefix(last_result)
|
80
|
+
# ftp.run "mv #{last_result} #{place_in}/#{newname}"
|
81
|
+
#
|
82
|
+
# ftp.cleanup_directory(place_in, how_many_to_keep_today)
|
83
|
+
# ftp.close
|
84
|
+
end
|
85
|
+
|
86
|
+
def rotate_via_s3(last_result)
|
87
|
+
s3 = Backup::S3Actor.new(c)
|
88
|
+
s3.verify_rotation_hierarchy_exists(hierarchy)
|
89
|
+
index = s3.rotation
|
90
|
+
index[todays_generation] << last_result
|
91
|
+
s3.rotation = index
|
92
|
+
s3.cleanup(todays_generation, how_many_to_keep_today)
|
93
|
+
end
|
94
|
+
|
95
|
+
def create_sons_today?; is_today_a? :son_created_on; end
|
96
|
+
def promote_sons_today?; is_today_a? :son_promoted_on; end
|
97
|
+
def promote_fathers_today?; is_today_a? :father_promoted_on; end
|
98
|
+
|
99
|
+
# old ( offset_days % c[:son_promoted_on] ) == 0 ? true : false
|
100
|
+
# old ( offset_days % (c[:son_promoted_on] * c[:father_promoted_on]) ) == 0 ? true : false
|
101
|
+
|
102
|
+
private
|
103
|
+
def is_today_a?(symbol)
|
104
|
+
t = $test_time || Date.today
|
105
|
+
day = DateParser.date_from( c[symbol] )
|
106
|
+
day.include?( t )
|
107
|
+
end
|
108
|
+
|
109
|
+
def verify_local_backup_directory_exists(dir)
|
110
|
+
path = c[:backup_path]
|
111
|
+
full = path + "/" + dir
|
112
|
+
unless File.exists?(full)
|
113
|
+
mkdir_p full
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
#def offset_days
|
118
|
+
# t = $test_time || Time.now # todo, write a test to use this
|
119
|
+
# num_from_day = Time.num_from_day( c[:promote_on] )
|
120
|
+
# offset = ( t.days_since_epoch + num_from_day + 0)
|
121
|
+
# offset
|
122
|
+
#end
|
123
|
+
|
124
|
+
def cleanup_via_mv(where, num_keep)
|
125
|
+
|
126
|
+
files = Dir[where + "/*"].sort
|
127
|
+
diff = files.size - num_keep
|
128
|
+
|
129
|
+
1.upto( diff ) do
|
130
|
+
extra = files.shift
|
131
|
+
rm extra
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def hierarchy
|
136
|
+
%w{grandfathers fathers sons}
|
137
|
+
end
|
138
|
+
|
139
|
+
# figure out which generation today's backup is
|
140
|
+
public
|
141
|
+
def todays_generation
|
142
|
+
goes_in = promote_fathers_today? ? "grandfathers" : \
|
143
|
+
promote_sons_today? ? "fathers" : "sons"
|
144
|
+
end
|
145
|
+
|
146
|
+
def self.timestamped_prefix(name,time="%Y-%m-%d-%H-%M-%S")
|
147
|
+
time ||= "%Y-%m-%d-%H-%M-%S" # there has to be a better way to do this
|
148
|
+
# but it works.
|
149
|
+
newname = Time.now.strftime(time) + "_" + File.basename(name)
|
150
|
+
end
|
151
|
+
|
152
|
+
# Given +name+ returns a timestamped version of name.
|
153
|
+
def timestamped_prefix(name)
|
154
|
+
Backup::Rotator.timestamped_prefix(name,c[:timestamp])
|
155
|
+
end
|
156
|
+
|
157
|
+
private
|
158
|
+
|
159
|
+
# Decide where to place the next backup. g,f,or s. See the standard
|
160
|
+
# config for a description of the difference between numeric and temporal
|
161
|
+
# rotation mode
|
162
|
+
def place_in
|
163
|
+
if :numeric == c[:rotation_mode]
|
164
|
+
goes_in = find_goes_in_by_state
|
165
|
+
else
|
166
|
+
goes_in = todays_generation
|
167
|
+
end
|
168
|
+
|
169
|
+
place_in = c[:backup_path] + "/" + goes_in
|
170
|
+
end
|
171
|
+
|
172
|
+
def find_goes_in_by_state
|
173
|
+
previous_sons = $state.system.sons_since_last_promotion || 0
|
174
|
+
previous_fathers = $state.system.fathers_since_last_promotion || 0
|
175
|
+
|
176
|
+
#puts "in find goes in"
|
177
|
+
#puts "sons since last: " + $state.system.sons_since_last_promotion.to_s
|
178
|
+
#puts "fathers since last: " + $state.system.fathers_since_last_promotion.to_s
|
179
|
+
|
180
|
+
if previous_sons >= c[:sons_promoted_after]
|
181
|
+
if previous_fathers >= c[:fathers_promoted_after]
|
182
|
+
return "grandfathers"
|
183
|
+
end
|
184
|
+
return "fathers"
|
185
|
+
end
|
186
|
+
"sons"
|
187
|
+
end
|
188
|
+
|
189
|
+
# Returns the number of sons to keep. Looks for config values +:sons_to_keep+,
|
190
|
+
# +:son_promoted_on+. Default +14+.
|
191
|
+
def sons_to_keep
|
192
|
+
c[:sons_to_keep] || c[:son_promoted_on] || 14
|
193
|
+
end
|
194
|
+
|
195
|
+
# Returns the number of fathers to keep. Looks for config values +:fathers_to_keep+,
|
196
|
+
# +:fathers_promoted_on+. Default +6+.
|
197
|
+
def fathers_to_keep
|
198
|
+
c[:fathers_to_keep] || c[:father_promoted_on] || 6
|
199
|
+
end
|
200
|
+
|
201
|
+
# Returns the number of grandfathers to keep. Looks for config values
|
202
|
+
# +:grandfathers_to_keep+. Default +6+.
|
203
|
+
def gfathers_to_keep
|
204
|
+
c[:grandfathers_to_keep] || 6
|
205
|
+
end
|
206
|
+
|
207
|
+
# This method returns the number of how many to keep in whatever today
|
208
|
+
# goes in. Example: if today is a day to create a +son+ then this
|
209
|
+
# function returns the value of +sons_to_keep+. If today is a +father+
|
210
|
+
# then +fathers_to_keep+ etc.
|
211
|
+
def how_many_to_keep_today
|
212
|
+
goes_in = todays_generation
|
213
|
+
keep = goes_in =~ /^sons$/ ? sons_to_keep :
|
214
|
+
goes_in =~ /^fathers$/ ? fathers_to_keep :
|
215
|
+
goes_in =~ /^grandfathers$/ ? gfathers_to_keep : 14
|
216
|
+
end
|
217
|
+
|
218
|
+
end
|
219
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
module Backup
|
4
|
+
class S3Actor
|
5
|
+
include AWS::S3
|
6
|
+
|
7
|
+
attr_accessor :rotation
|
8
|
+
|
9
|
+
attr_reader :config
|
10
|
+
alias_method :c, :config
|
11
|
+
|
12
|
+
def initialize(config)
|
13
|
+
@config = config
|
14
|
+
@rotation_key = c[:rotation_object_key] ||= 'backup_rotation_index.yml'
|
15
|
+
@access_key = c[:aws_access] ||= ENV['AMAZON_ACCESS_KEY_ID']
|
16
|
+
@secret_key = c[:aws_secret] ||= ENV['AMAZON_SECRET_ACCESS_KEY']
|
17
|
+
@bucket_key = "#{@access_key}.#{c[:backup_path]}"
|
18
|
+
Base.establish_connection!(
|
19
|
+
:access_key_id => @access_key,
|
20
|
+
:secret_access_key => @secret_key
|
21
|
+
)
|
22
|
+
begin
|
23
|
+
# Look for our bucket, if it's not there, try to create it.
|
24
|
+
@bucket = Bucket.find @bucket_key
|
25
|
+
rescue NoSuchBucket
|
26
|
+
@bucket = Bucket.create @bucket_key
|
27
|
+
@bucket = Bucket.find @bucket_key
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def rotation
|
32
|
+
object = S3Object.find(@rotation_key, @bucket.name)
|
33
|
+
index = YAML::load(object.value)
|
34
|
+
end
|
35
|
+
|
36
|
+
def rotation=(index)
|
37
|
+
object = S3Object.store(@rotation_key, index.to_yaml, @bucket.name)
|
38
|
+
index
|
39
|
+
end
|
40
|
+
|
41
|
+
# Send a file to s3
|
42
|
+
def put(last_result)
|
43
|
+
puts last_result
|
44
|
+
object_key = Rotator.timestamped_prefix(last_result)
|
45
|
+
S3Object.store object_key,
|
46
|
+
open(last_result),
|
47
|
+
@bucket.name
|
48
|
+
object_key
|
49
|
+
end
|
50
|
+
|
51
|
+
# Remove a file from s3
|
52
|
+
def delete(object_key)
|
53
|
+
S3Object.delete object_key, @bucket.name
|
54
|
+
end
|
55
|
+
|
56
|
+
# Make sure our rotation index exists and contains the hierarchy we're using.
|
57
|
+
# Create it if it does not exist
|
58
|
+
def verify_rotation_hierarchy_exists(hierarchy)
|
59
|
+
begin
|
60
|
+
index = self.rotation
|
61
|
+
verified_index = index.merge(init_rotation_index(hierarchy)) { |m,x,y| x ||= y }
|
62
|
+
unless (verified_index == index)
|
63
|
+
self.rotation = verified_index
|
64
|
+
end
|
65
|
+
rescue NoSuchKey
|
66
|
+
self.rotation = init_rotation_index(hierarchy)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Expire old objects
|
71
|
+
def cleanup(generation, keep)
|
72
|
+
puts "Cleaning up"
|
73
|
+
|
74
|
+
keys = self.rotation[generation]
|
75
|
+
diff = keys.size - keep
|
76
|
+
|
77
|
+
1.upto( diff ) do
|
78
|
+
extra_key = keys.shift
|
79
|
+
delete extra_key
|
80
|
+
end
|
81
|
+
# store updated index
|
82
|
+
self.rotation = keys
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
# Create a new index representing our backup hierarchy
|
88
|
+
def init_rotation_index(hierarchy)
|
89
|
+
hash = {}
|
90
|
+
hierarchy.each do |m|
|
91
|
+
hash[m] = Array.new
|
92
|
+
end
|
93
|
+
hash
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,139 @@
|
|
1
|
+
# large portions borrowed from capistrano
|
2
|
+
require 'rubygems'
|
3
|
+
require 'net/ssh'
|
4
|
+
|
5
|
+
# TODO - add in a way to extend the belay script to work with this. thats a
|
6
|
+
# good idea for the belay script over all. write the kernal extensions, and the
|
7
|
+
# rake extensions. form there write an example third party extension.
|
8
|
+
module Backup
|
9
|
+
class Command
|
10
|
+
attr_reader :command, :options
|
11
|
+
attr_reader :actor
|
12
|
+
|
13
|
+
def initialize(server_name, command, callback, options, actor) #:nodoc:
|
14
|
+
@command = command.strip.gsub(/\r?\n/, "\\\n")
|
15
|
+
@callback = callback
|
16
|
+
@options = options
|
17
|
+
@actor = actor
|
18
|
+
@channels = open_channels
|
19
|
+
end
|
20
|
+
|
21
|
+
def process!
|
22
|
+
since = Time.now
|
23
|
+
loop do
|
24
|
+
active = 0
|
25
|
+
@channels.each do |ch|
|
26
|
+
next if ch[:closed]
|
27
|
+
active += 1
|
28
|
+
ch.connection.process(true)
|
29
|
+
end
|
30
|
+
|
31
|
+
break if active == 0
|
32
|
+
if Time.now - since >= 1
|
33
|
+
since = Time.now
|
34
|
+
@channels.each { |ch| ch.connection.ping! }
|
35
|
+
end
|
36
|
+
sleep 0.01 # a brief respite, to keep the CPU from going crazy
|
37
|
+
end
|
38
|
+
|
39
|
+
#logger.trace "command finished"
|
40
|
+
|
41
|
+
if failed = @channels.detect { |ch| ch[:status] != 0 }
|
42
|
+
raise "command #{@command.inspect} failed"
|
43
|
+
end
|
44
|
+
|
45
|
+
self
|
46
|
+
end
|
47
|
+
|
48
|
+
def open_channels
|
49
|
+
channel = actor.session.open_channel do |channel|
|
50
|
+
channel.request_pty( :want_reply => true )
|
51
|
+
channel[:actor] = @actor
|
52
|
+
|
53
|
+
channel.on_success do |ch|
|
54
|
+
#logger.trace "executing command", ch[:host]
|
55
|
+
ch.exec command
|
56
|
+
ch.send_data options[:data] if options[:data]
|
57
|
+
end
|
58
|
+
|
59
|
+
channel.on_data do |ch, data|
|
60
|
+
puts data
|
61
|
+
@callback[ch, :out, data] if @callback
|
62
|
+
end
|
63
|
+
|
64
|
+
channel.on_failure do |ch|
|
65
|
+
#logger.important "could not open channel", ch[:host]
|
66
|
+
# puts "we got a faulure"
|
67
|
+
ch.close
|
68
|
+
end
|
69
|
+
|
70
|
+
channel.on_request do |ch, request, reply, data|
|
71
|
+
ch[:status] = data.read_long if request == "exit-status"
|
72
|
+
end
|
73
|
+
|
74
|
+
channel.on_close do |ch|
|
75
|
+
ch[:closed] = true
|
76
|
+
end
|
77
|
+
end
|
78
|
+
[channel]
|
79
|
+
end
|
80
|
+
end # end Class Command
|
81
|
+
|
82
|
+
class SshActor
|
83
|
+
|
84
|
+
#def self.new_for_ssh(server_name)
|
85
|
+
# a = new(server_name)
|
86
|
+
#end
|
87
|
+
|
88
|
+
attr_reader :session
|
89
|
+
attr_reader :config
|
90
|
+
alias_method :c, :config
|
91
|
+
|
92
|
+
def initialize(config)
|
93
|
+
@config = config
|
94
|
+
end
|
95
|
+
|
96
|
+
def connect
|
97
|
+
c[:servers].each do |server| # todo, make this actually work
|
98
|
+
@session = Net::SSH.start(
|
99
|
+
server,
|
100
|
+
c[:ssh_user],
|
101
|
+
:port => c[:port],
|
102
|
+
:host_key => "ssh-rsa",
|
103
|
+
:keys => [ c[:identity_key] ],
|
104
|
+
:auth_methods => %w{ publickey } )
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def run(cmd, options={}, &block)
|
109
|
+
#logger.debug "executing #{cmd.strip.inspect}"
|
110
|
+
puts "executing #{cmd.strip.inspect}"
|
111
|
+
command = Command.new(@server_name, cmd, block, options, self)
|
112
|
+
command.process! # raises an exception if command fails on any server
|
113
|
+
end
|
114
|
+
|
115
|
+
def on_remote(&block)
|
116
|
+
connect
|
117
|
+
self.instance_eval(&block)
|
118
|
+
close
|
119
|
+
end
|
120
|
+
|
121
|
+
def close
|
122
|
+
@session.close
|
123
|
+
end
|
124
|
+
|
125
|
+
def verify_directory_exists(dir)
|
126
|
+
run "if [ -d '#{dir}' ]; then true; else mkdir -p '#{dir}'; fi"
|
127
|
+
end
|
128
|
+
|
129
|
+
def cleanup_directory(dir, keep)
|
130
|
+
puts "Cleaning up"
|
131
|
+
cleanup = <<-END
|
132
|
+
LOOK_IN="#{dir}"; COUNT=`ls -1 $LOOK_IN | wc -l`; MAX=#{keep}; if (( $COUNT > $MAX )); then let "OFFSET=$COUNT-$MAX"; i=1; for f in `ls -1 $LOOK_IN | sort`; do if (( $i <= $OFFSET )); then CMD="rm $LOOK_IN/$f"; echo $CMD; $CMD; fi; let "i=$i + 1"; done; else true; fi
|
133
|
+
END
|
134
|
+
run cleanup # todo make it so that even this can be overridden
|
135
|
+
end
|
136
|
+
|
137
|
+
end # end class SshActor
|
138
|
+
|
139
|
+
end # end Module Backup
|