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.
@@ -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