backupgem 0.0.9 → 0.0.11

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