backupgem 0.0.9 → 0.0.11
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +4 -0
- data/Rakefile +2 -2
- data/bin/commands.sh +2 -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/actor.rb.orig +200 -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/cleanup.sh +2 -0
- data/tests/tests_helper.rb +5 -0
- metadata +31 -3
@@ -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
|
+
:port => c[:port],
|
101
|
+
:username => c[:ssh_user],
|
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
|