ey_cloud_server 1.4.5 → 1.4.26
Sign up to get free protection for your applications and to get access to all the features.
- data/bin/binary_log_purge +10 -2
- data/bin/ey-snapshots +7 -1
- data/bin/eybackup +13 -1
- data/lib/ey-flex.rb +18 -7
- data/lib/ey-flex/big-brother.rb +2 -2
- data/lib/ey-flex/bucket_minder.rb +46 -160
- data/lib/ey-flex/ec2.rb +17 -0
- data/lib/ey-flex/snapshot_minder.rb +93 -171
- data/lib/ey_backup.rb +84 -48
- data/lib/ey_backup/backend.rb +34 -0
- data/lib/ey_backup/backup_set.rb +70 -63
- data/lib/ey_backup/base.rb +0 -5
- data/lib/ey_backup/cli.rb +26 -6
- data/lib/ey_backup/database.rb +48 -0
- data/lib/ey_backup/dumper.rb +15 -31
- data/lib/ey_backup/engine.rb +7 -17
- data/lib/ey_backup/engines/mysql_engine.rb +24 -16
- data/lib/ey_backup/engines/postgresql_engine.rb +26 -20
- data/lib/ey_backup/loader.rb +13 -33
- data/lib/ey_backup/processors/gpg_encryptor.rb +3 -20
- data/lib/ey_backup/processors/gzipper.rb +0 -29
- data/lib/ey_backup/processors/splitter.rb +22 -34
- data/lib/ey_backup/spawner.rb +7 -13
- data/lib/ey_cloud_server.rb +1 -1
- data/lib/{ey-flex → ey_cloud_server}/version.rb +1 -1
- data/spec/big-brother_spec.rb +12 -0
- data/spec/bucket_minder_spec.rb +113 -0
- data/spec/config-example.yml +11 -0
- data/spec/ey_api_spec.rb +63 -0
- data/spec/ey_backup/backend_spec.rb +12 -0
- data/spec/ey_backup/backup_spec.rb +54 -0
- data/spec/ey_backup/cli_spec.rb +35 -0
- data/spec/ey_backup/mysql_backups_spec.rb +208 -0
- data/spec/ey_backup/postgres_backups_spec.rb +106 -0
- data/spec/ey_backup/spec_helper.rb +5 -0
- data/spec/fakefs_hax.rb +50 -0
- data/spec/gpg.public +0 -0
- data/spec/gpg.sekrit +0 -0
- data/spec/helpers.rb +270 -0
- data/spec/snapshot_minder_spec.rb +68 -0
- data/spec/spec_helper.rb +31 -0
- metadata +286 -53
data/bin/binary_log_purge
CHANGED
@@ -100,14 +100,22 @@ def run_query(host, user, password, query)
|
|
100
100
|
if host == '127.0.0.1'
|
101
101
|
options = options + ' -P13306'
|
102
102
|
end
|
103
|
-
|
103
|
+
if query == 'show processlist'
|
104
|
+
stdin, stdout, stderr = Open3.popen3("mysql -u#{user} -p#{password} #{options} -h#{host} -N -e\"#{query}\"|grep 'Binlog'")
|
105
|
+
else
|
106
|
+
stdin, stdout, stderr = Open3.popen3("mysql -u#{user} -p#{password} #{options} -h#{host} -N -e\"#{query}\"")
|
107
|
+
end
|
104
108
|
query_error = stderr.read
|
105
109
|
if query_error.length > 0
|
106
110
|
log_error "Error caught: #{query_error}"
|
107
111
|
test_add_privilege(user, password, query_error)
|
108
112
|
exit 0
|
109
113
|
end
|
110
|
-
|
114
|
+
result = stdout.read
|
115
|
+
stdin.close
|
116
|
+
stdout.close
|
117
|
+
stderr.close
|
118
|
+
result
|
111
119
|
end
|
112
120
|
|
113
121
|
# function to test for user privilege
|
data/bin/ey-snapshots
CHANGED
@@ -1,9 +1,15 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# Disable verbose (when running from cron) to squash warnings output by rest_client (1.6.1)
|
4
|
+
unless STDOUT.isatty
|
5
|
+
$VERBOSE = nil
|
6
|
+
end
|
7
|
+
|
2
8
|
require File.dirname(__FILE__) + '/../lib/ey-flex'
|
3
9
|
|
4
10
|
begin
|
5
11
|
EY::SnapshotMinder.run(ARGV)
|
6
12
|
rescue => e
|
7
|
-
EY.
|
13
|
+
EY.notify_snapshot_error(e)
|
8
14
|
raise
|
9
15
|
end
|
data/bin/eybackup
CHANGED
@@ -1,6 +1,18 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
+
# Disable verbose (when running from cron) to squash warnings output by rest_client (1.6.1) and also fog (0.4.0)
|
4
|
+
unless STDOUT.isatty
|
5
|
+
$VERBOSE = nil
|
6
|
+
end
|
7
|
+
|
3
8
|
require 'rubygems'
|
4
9
|
require File.dirname(__FILE__) + '/../lib/ey_backup'
|
10
|
+
require File.dirname(__FILE__) + '/../lib/ey-flex'
|
11
|
+
|
12
|
+
begin
|
13
|
+
EY::Backup.run(ARGV)
|
14
|
+
rescue => e
|
15
|
+
EY.notify_backup_error(e)
|
16
|
+
raise
|
17
|
+
end
|
5
18
|
|
6
|
-
EY::Backup.run(ARGV)
|
data/lib/ey-flex.rb
CHANGED
@@ -14,19 +14,29 @@ require 'stringio'
|
|
14
14
|
require 'yaml'
|
15
15
|
require "optparse"
|
16
16
|
require 'open4'
|
17
|
+
require "ey_enzyme"
|
17
18
|
|
18
19
|
lib_dir = File.expand_path(__FILE__ + '/../ey-flex')
|
19
20
|
|
20
21
|
module EY
|
21
|
-
def self.
|
22
|
-
enzyme_api.notify_error("
|
22
|
+
def self.notify_snapshot_error(error)
|
23
|
+
enzyme_api.notify_error("snapshot", error)
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.notify_backup_error(error)
|
27
|
+
enzyme_api.notify_error("backup", error)
|
23
28
|
end
|
24
29
|
|
25
30
|
def self.enzyme_api
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
31
|
+
@enzyme_api ||= EY::Enzyme::API.new(
|
32
|
+
enzyme_config[:api],
|
33
|
+
enzyme_config[:instance_id],
|
34
|
+
enzyme_config[:token]
|
35
|
+
)
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.enzyme_config
|
39
|
+
@enzyme_config ||= YAML.load_file("/etc/engineyard/dracul.yml")
|
30
40
|
end
|
31
41
|
|
32
42
|
module Flex
|
@@ -39,4 +49,5 @@ require lib_dir + '/big-brother'
|
|
39
49
|
require lib_dir + '/bucket_minder'
|
40
50
|
require lib_dir + '/ey-api'
|
41
51
|
require lib_dir + '/snapshot_minder'
|
42
|
-
require lib_dir + '/
|
52
|
+
require lib_dir + '/ec2'
|
53
|
+
require lib_dir + '/../ey_cloud_server/version'
|
data/lib/ey-flex/big-brother.rb
CHANGED
@@ -1,194 +1,80 @@
|
|
1
|
-
module AWS::S3
|
2
|
-
class S3Object
|
3
|
-
def <=>(other)
|
4
|
-
DateTime.parse(self.about['last-modified']) <=> DateTime.parse(other.about['last-modified'])
|
5
|
-
end
|
6
|
-
end
|
7
|
-
end
|
8
|
-
|
9
1
|
require 'open-uri'
|
10
2
|
|
11
3
|
module EY
|
12
|
-
|
13
4
|
class BucketMinder
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
)
|
21
|
-
@instance_id = opts[:instance_id]
|
22
|
-
@type = opts[:type]
|
23
|
-
@env = opts[:env]
|
24
|
-
@opts = opts
|
25
|
-
@bucket = find_bucket(opts[:bucket])
|
26
|
-
|
27
|
-
opts[:extension] ||= "tgz"
|
28
|
-
@keep = opts[:keep]
|
29
|
-
@name = "#{Time.now.strftime("%Y-%m-%dT%H:%M:%S").gsub(/:/, '-')}.#{@type}.#{opts[:extension]}"
|
30
|
-
end
|
31
|
-
|
32
|
-
def find_bucket(buck="#{@env}-#{@type}-#{@instance_id}-#{Digest::SHA1.hexdigest(@opts[:aws_secret_id])[0..6]}")
|
33
|
-
begin
|
34
|
-
begin
|
35
|
-
AWS::S3::Bucket.find(buck)
|
36
|
-
rescue AWS::S3::NoSuchBucket
|
37
|
-
begin
|
38
|
-
AWS::S3::Bucket.create(buck)
|
39
|
-
rescue AWS::S3::ResponseError
|
40
|
-
end
|
41
|
-
end
|
42
|
-
buck
|
43
|
-
end
|
44
|
-
end
|
45
|
-
|
46
|
-
def instance_id
|
47
|
-
@instance_id ||= open("http://169.254.169.254/latest/meta-data/instance-id").read
|
48
|
-
end
|
49
|
-
|
50
|
-
def upload_object(file, printer = true)
|
51
|
-
AWS::S3::S3Object.store(
|
52
|
-
@name,
|
53
|
-
File.open(file),
|
54
|
-
bucket,
|
55
|
-
:access => :private
|
56
|
-
)
|
57
|
-
FileUtils.rm file
|
58
|
-
puts "successful upload: #{@name}" if printer
|
59
|
-
true
|
60
|
-
end
|
61
|
-
|
62
|
-
def remove_object(key, printer = false)
|
63
|
-
begin
|
64
|
-
AWS::S3::S3Object.delete(key, @bucket)
|
65
|
-
puts "Deleting #{key}" if printer
|
66
|
-
# S3's eventual consistency sometimes causes really weird
|
67
|
-
# failures.
|
68
|
-
# Since cleanup happens every time and will clean up all stale
|
69
|
-
# objects, we can just ignore S3-interaction failures. It'll
|
70
|
-
# work next time.
|
71
|
-
rescue AWS::S3::S3Exception, AWS::S3::Error
|
72
|
-
nil
|
73
|
-
end
|
5
|
+
def initialize(secret_id, secret_key, bucket_name, region = 'us-east-1')
|
6
|
+
@s3 = Fog::Storage.new(:provider => 'AWS',:aws_access_key_id => secret_id, :aws_secret_access_key => secret_key, :region => region)
|
7
|
+
@region = region
|
8
|
+
@bucket_name = bucket_name || "ey-backup-#{Digest::SHA1.hexdigest(secret_id)[0..11]}"
|
9
|
+
|
10
|
+
setup_bucket
|
74
11
|
end
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
12
|
+
|
13
|
+
attr_reader :bucket_name
|
14
|
+
|
15
|
+
def bucket
|
16
|
+
@bucket ||= @s3.directories.get(@bucket_name)
|
79
17
|
end
|
80
|
-
|
81
|
-
def
|
82
|
-
|
83
|
-
puts "downloading: #{obj}" if printer
|
84
|
-
File.open(obj.key, 'wb') do |f|
|
85
|
-
print "." if printer
|
86
|
-
obj.value {|chunk| f.write chunk }
|
87
|
-
end
|
88
|
-
puts if printer
|
89
|
-
puts "finished" if printer
|
90
|
-
obj.key
|
18
|
+
|
19
|
+
def files
|
20
|
+
bucket.files
|
91
21
|
end
|
92
22
|
|
93
|
-
def
|
94
|
-
|
95
|
-
|
23
|
+
def setup_bucket
|
24
|
+
unless bucket
|
25
|
+
@s3.directories.create(s3_params(:key => @bucket_name))
|
96
26
|
end
|
97
27
|
end
|
98
|
-
|
99
|
-
def
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
# S3's eventual consistency sometimes causes really weird
|
106
|
-
# failures.
|
107
|
-
# Since cleanup happens every time and will clean up all stale
|
108
|
-
# objects, we can just ignore S3-interaction failures. It'll
|
109
|
-
# work next time.
|
110
|
-
rescue AWS::S3::S3Exception, AWS::S3::Error
|
111
|
-
nil
|
28
|
+
|
29
|
+
def s3_params(params = {})
|
30
|
+
return params if @region == 'us-east-1'
|
31
|
+
if @region == 'eu-west-1'
|
32
|
+
params.merge({:location => 'EU'})
|
33
|
+
else
|
34
|
+
params.merge({:location => @region})
|
112
35
|
end
|
113
36
|
end
|
114
|
-
|
115
|
-
def
|
116
|
-
|
117
|
-
File.expand_path(name)
|
118
|
-
end
|
119
|
-
|
120
|
-
# Removes all of the items in the current bucket
|
121
|
-
def clear_bucket
|
122
|
-
list.each do |o|
|
123
|
-
puts "deleting: #{o.key}"
|
124
|
-
o.delete
|
125
|
-
end
|
126
|
-
end
|
127
|
-
|
128
|
-
# Deletes the most recent file from the bucket
|
129
|
-
def rollback
|
130
|
-
o = list.last
|
131
|
-
puts "rolling back: #{o.key}"
|
132
|
-
o.delete
|
37
|
+
|
38
|
+
def remove_object(key)
|
39
|
+
@s3.delete_object(bucket.key, key)
|
133
40
|
end
|
134
|
-
|
135
|
-
def
|
136
|
-
|
41
|
+
|
42
|
+
def stream(key, &block)
|
43
|
+
files.get(key, &block)
|
137
44
|
end
|
138
|
-
|
139
|
-
def list(directive = false)
|
140
|
-
if directive.is_a? Hash
|
141
|
-
prefix = directive[:prefix]
|
142
|
-
printer = directive[:print] ||= false
|
143
|
-
listing = AWS::S3::Bucket.objects(bucket, :prefix => prefix).flatten.sort
|
144
|
-
objects = s3merge(listing)
|
145
|
-
elsif directive.is_a? String
|
146
|
-
printer = directive
|
147
|
-
objects = AWS::S3::Bucket.objects(bucket).sort
|
148
|
-
else
|
149
|
-
puts "Input to list method must be Hash or String not: #{printer.class}"
|
150
|
-
exit
|
151
|
-
end
|
152
45
|
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
if printer
|
158
|
-
objects.each_with_index do |b, i|
|
159
|
-
if b.is_a? Hash
|
160
|
-
puts "#{i}:#{@env} #{b[:name]}"
|
161
|
-
else
|
162
|
-
puts "#{i}:#{@env} #{b.key}"
|
163
|
-
end
|
164
|
-
end
|
165
|
-
end
|
166
|
-
objects
|
46
|
+
def list(prefix)
|
47
|
+
listing = files.all(:prefix => prefix)
|
48
|
+
s3merge(listing)
|
167
49
|
end
|
168
|
-
|
50
|
+
|
169
51
|
# Merge s3 file listing to work with split files with naming of *.part\d\d
|
170
52
|
def s3merge(list)
|
171
53
|
return list if list.empty?
|
172
|
-
|
54
|
+
distinct_files=Array.new()
|
55
|
+
|
173
56
|
list.each do |item|
|
174
57
|
fname = item.key.gsub(/.part\d+$/,'')
|
175
58
|
match = false
|
176
|
-
|
59
|
+
distinct_files.each_with_index do |b, i|
|
177
60
|
if b[:name] == fname
|
178
|
-
|
61
|
+
distinct_files[i][:keys] << item.key
|
179
62
|
match = true
|
180
63
|
end
|
181
64
|
end
|
182
|
-
|
65
|
+
|
183
66
|
if not match
|
184
67
|
path = Array.new()
|
185
68
|
path << item.key
|
186
69
|
file = {:name => fname, :keys => path}
|
187
|
-
|
70
|
+
distinct_files << file
|
188
71
|
end
|
189
72
|
end
|
190
|
-
|
73
|
+
distinct_files
|
74
|
+
end
|
75
|
+
|
76
|
+
def put(filename, contents)
|
77
|
+
files.create(:key => filename, :body => contents)
|
191
78
|
end
|
192
|
-
|
193
79
|
end
|
194
|
-
end
|
80
|
+
end
|
data/lib/ey-flex/ec2.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
module EY
|
2
|
+
class EC2
|
3
|
+
def initialize(opts = {})
|
4
|
+
@ec2 = RightAws::Ec2.new(opts[:aws_secret_id], opts[:aws_secret_key], :logger => Logger.new("/dev/null"))
|
5
|
+
end
|
6
|
+
|
7
|
+
def method_missing(method, *a, &b)
|
8
|
+
@ec2.send(method, *a, &b)
|
9
|
+
rescue RightAws::AwsError => e
|
10
|
+
retries ||= 10
|
11
|
+
retries -= 1
|
12
|
+
raise e if retries == 0
|
13
|
+
sleep 30
|
14
|
+
retry
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -1,11 +1,64 @@
|
|
1
|
+
require 'ey_instance_api_client'
|
1
2
|
module EY
|
2
3
|
class SnapshotMinder
|
3
|
-
def self.run(args)
|
4
|
-
defaults = {:config => '/etc/.mysql.backups.yml',
|
5
|
-
:command => :list_snapshots,
|
6
|
-
:keep => 5}
|
7
4
|
|
8
|
-
|
5
|
+
class Backend
|
6
|
+
class Mock
|
7
|
+
def initialize
|
8
|
+
@commands_called = []
|
9
|
+
@raise_count = 0
|
10
|
+
@total_wait_time = 0
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_reader :commands_called, :total_wait_time
|
14
|
+
attr_accessor :raise_count
|
15
|
+
|
16
|
+
def wait(seconds)
|
17
|
+
@total_wait_time += seconds
|
18
|
+
end
|
19
|
+
|
20
|
+
def snapshot
|
21
|
+
if @raise_count > 0
|
22
|
+
@raise_count -= 1
|
23
|
+
raise EY::InstanceAPIClient::Connection::UnexpectedStatus.new("")
|
24
|
+
else
|
25
|
+
@commands_called << "request_snapshot"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def list
|
30
|
+
@commands_called << "list_snapshots"
|
31
|
+
[EY::InstanceAPIClient::Snapshot.new({
|
32
|
+
'state' => 'in progres',
|
33
|
+
'progress' => '0%',
|
34
|
+
'volume_type' => 'db',
|
35
|
+
'snapshot_id' => 12,
|
36
|
+
'created_at' => Time.now.to_s
|
37
|
+
})]
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
class Real
|
42
|
+
def initialize
|
43
|
+
@snapshots = EY::InstanceAPIClient::Snapshots.new
|
44
|
+
end
|
45
|
+
|
46
|
+
def wait(seconds)
|
47
|
+
sleep(seconds)
|
48
|
+
end
|
49
|
+
|
50
|
+
def snapshot
|
51
|
+
@snapshots.request
|
52
|
+
end
|
53
|
+
|
54
|
+
def list
|
55
|
+
@snapshots.list
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.run(args)
|
61
|
+
options = {:command => :list_snapshots}
|
9
62
|
# Build a parser for the command line arguments
|
10
63
|
opts = OptionParser.new do |opts|
|
11
64
|
opts.version = "0.0.1"
|
@@ -18,16 +71,15 @@ module EY
|
|
18
71
|
options[:command] = :list_snapshots
|
19
72
|
end
|
20
73
|
|
21
|
-
opts.on("-c", "--config CONFIG", "
|
22
|
-
|
74
|
+
opts.on("-c", "--config CONFIG", "DEPRECATED, does nothing") do |config|
|
75
|
+
STDERR.puts "WARNING: --config is DEPRECATED and has no effect"
|
23
76
|
end
|
24
77
|
|
25
|
-
opts.on("-i", "--instance-id ID", "
|
26
|
-
|
78
|
+
opts.on("-i", "--instance-id ID", "DEPRECATED, does nothing") do |iid|
|
79
|
+
STDERR.puts "WARNING: --instance-id is DEPRECATED and has no effect"
|
27
80
|
end
|
28
81
|
|
29
|
-
|
30
|
-
opts.on("--snapshot", "take snapshots of both of your volumes(only runs on your ec2 instance)") do
|
82
|
+
opts.on("--snapshot", "request a snapshot of the volumes attached to this instance") do
|
31
83
|
options[:command] = :snapshot_volumes
|
32
84
|
end
|
33
85
|
|
@@ -38,186 +90,56 @@ module EY
|
|
38
90
|
end
|
39
91
|
|
40
92
|
opts.parse!(args)
|
41
|
-
|
42
|
-
ey = nil
|
43
|
-
if File.exist?(config = File.expand_path(defaults[:config]))
|
44
|
-
ey = new(options = defaults.merge(YAML::load(File.read(config))).merge(options))
|
45
|
-
else
|
46
|
-
abort "You need to have an /etc/.mysql.backups.yml file with your credentials in it to use this tool.\nOr point it at a yaml file with -c .mysql.backups.yml"
|
47
|
-
end
|
48
|
-
|
49
|
-
ey.send(options[:command])
|
50
|
-
ey.clean_snapshots(options[:keep])
|
93
|
+
new(options).send(options[:command])
|
51
94
|
end
|
52
95
|
|
53
|
-
def
|
54
|
-
@
|
55
|
-
@instance_id = opts[:instance_id]
|
56
|
-
@db = Mysql.new('root', opts[:dbpass], opts[:lock_wait_timeout]) rescue nil
|
57
|
-
@ec2 = RightAws::Ec2.new(opts[:aws_secret_id], opts[:aws_secret_key], :logger => Logger.new("/dev/null"))
|
58
|
-
get_instance_id
|
59
|
-
silence_stream($stderr) { find_volume_ids }
|
96
|
+
def self.backend
|
97
|
+
@backend ||= Backend::Real.new
|
60
98
|
end
|
61
99
|
|
62
|
-
def
|
63
|
-
@
|
64
|
-
@ec2.describe_volumes.each do |volume|
|
65
|
-
if volume[:aws_instance_id] == @instance_id
|
66
|
-
if volume[:aws_device] == "/dev/sdz1"
|
67
|
-
@volume_ids[:data] = volume[:aws_id]
|
68
|
-
elsif volume[:aws_device] == "/dev/sdz2"
|
69
|
-
@volume_ids[:db] = volume[:aws_id]
|
70
|
-
end
|
71
|
-
end
|
72
|
-
end
|
73
|
-
say "Volume IDs are #{@volume_ids.inspect}"
|
74
|
-
@volume_ids
|
100
|
+
def self.enable_mock!
|
101
|
+
@backend = Backend::Mock.new
|
75
102
|
end
|
76
103
|
|
77
|
-
def
|
78
|
-
@
|
79
|
-
@ec2.describe_snapshots.sort { |a,b| b[:aws_started_at] <=> a[:aws_started_at] }.each do |snapshot|
|
80
|
-
@volume_ids.each do |mnt, vol|
|
81
|
-
if snapshot[:aws_volume_id] == vol
|
82
|
-
(@snapshot_ids[mnt] ||= []) << snapshot[:aws_id]
|
83
|
-
end
|
84
|
-
end
|
85
|
-
end
|
86
|
-
say "Snapshots #{@snapshot_ids.inspect}"
|
87
|
-
@snapshot_ids
|
88
|
-
end
|
89
|
-
|
90
|
-
def clean_snapshots(keep=5)
|
91
|
-
list_snapshots
|
92
|
-
@snapshot_ids.each do |mnt, ids|
|
93
|
-
snaps = []
|
94
|
-
@ec2.describe_snapshots(ids).sort { |a,b| b[:aws_started_at] <=> a[:aws_started_at] }.each do |snapshot|
|
95
|
-
snaps << snapshot
|
96
|
-
end
|
97
|
-
(snaps[keep..-1]||[]).each do |snapshot|
|
98
|
-
say "deleting snapshot of /#{mnt}: #{snapshot[:aws_id]}"
|
99
|
-
@ec2.delete_snapshot(snapshot[:aws_id])
|
100
|
-
end
|
101
|
-
end
|
102
|
-
list_snapshots
|
104
|
+
def initialize(opts={})
|
105
|
+
@opts = opts
|
103
106
|
end
|
104
107
|
|
105
108
|
def snapshot_volumes
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
say "
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
109
|
+
retry_count = 0
|
110
|
+
begin
|
111
|
+
SnapshotMinder.backend.snapshot
|
112
|
+
say "Snapshot requested."
|
113
|
+
rescue EY::InstanceAPIClient::Connection::UnexpectedStatus => e
|
114
|
+
retry_count += 1
|
115
|
+
if retry_count <= 10
|
116
|
+
retry_in = backoff(retry_count)
|
117
|
+
say "failed with #{e.inspect}, retrying in #{retry_in} seconds"
|
118
|
+
SnapshotMinder.backend.wait(retry_in)
|
119
|
+
retry
|
120
|
+
else
|
121
|
+
raise e
|
119
122
|
end
|
120
123
|
end
|
121
|
-
snaps
|
122
|
-
end
|
123
|
-
|
124
|
-
def get_instance_id
|
125
|
-
return @instance_id if @instance_id
|
126
|
-
|
127
|
-
open('http://169.254.169.254/latest/meta-data/instance-id') do |f|
|
128
|
-
@instance_id = f.gets
|
129
|
-
end
|
130
|
-
abort "Cannot find instance id!" unless @instance_id
|
131
|
-
say "Instance ID is #{@instance_id}"
|
132
|
-
@instance_id
|
133
124
|
end
|
134
125
|
|
135
|
-
def
|
136
|
-
|
137
|
-
system(sync_cmd)
|
126
|
+
def backoff(nth_time)
|
127
|
+
60*nth_time + rand((60 / (nth_time+1)) + 5) #see tests
|
138
128
|
end
|
139
129
|
|
140
|
-
def
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
retries += 1
|
146
|
-
raise if retries > 10
|
147
|
-
sleep retries * retries
|
148
|
-
retry
|
130
|
+
def list_snapshots
|
131
|
+
snapshots = SnapshotMinder.backend.list
|
132
|
+
say "#{snapshots.size} Snapshots available:"
|
133
|
+
snapshots.each do |s|
|
134
|
+
say "#{s.id} - #{s.volume} #{s.state} #{s.progress} #{s.created_at}"
|
149
135
|
end
|
150
|
-
say "Created snapshot of #{volume_id} as #{snap[:aws_id]}"
|
151
|
-
snap
|
152
136
|
end
|
153
137
|
|
154
138
|
private
|
155
|
-
def say(msg, newline = true)
|
156
|
-
return if @opts[:quiet]
|
157
|
-
print("#{msg}#{"\n" if newline}")
|
158
|
-
end
|
159
|
-
|
160
|
-
def silence_stream(stream)
|
161
|
-
old_stream = stream.dup
|
162
|
-
stream.reopen("/dev/null")
|
163
|
-
stream.sync = true
|
164
|
-
yield
|
165
|
-
ensure
|
166
|
-
stream.reopen(old_stream)
|
167
|
-
end
|
168
|
-
end
|
169
|
-
|
170
|
-
class Mysql
|
171
|
-
|
172
|
-
attr_accessor :dbh
|
173
|
-
|
174
|
-
def initialize(username, password, lock_wait_timeout)
|
175
|
-
@username = username
|
176
|
-
@password = password
|
177
|
-
@read_lock_pid = nil
|
178
|
-
@lock_wait_timeout = lock_wait_timeout.nil? ? 5 : lock_wait_timeout
|
179
|
-
@master_status_file = "/db/mysql/.snapshot_backup_master_status.txt"
|
180
|
-
end
|
181
|
-
|
182
|
-
def waiting_read_lock_thread
|
183
|
-
thread_cmd = "mysql -p#{@password} -u #{@username} -N -e 'show full processlist;' | grep 'flush tables with read lock' | awk '{print $1}'"
|
184
|
-
%x{#{thread_cmd}}
|
185
|
-
end
|
186
|
-
|
187
|
-
def write_master_status
|
188
|
-
master_status_cmd = "mysql -p#{@password} -u #{@username} -e'SHOW MASTER STATUS\\G' > #{@master_status_file}"
|
189
|
-
system(master_status_cmd)
|
190
|
-
end
|
191
|
-
|
192
|
-
def flush_tables_with_read_lock
|
193
|
-
pipe = IO.popen("mysql -u #{@username} -p#{@password}", 'w')
|
194
|
-
@read_lock_pid = pipe.pid
|
195
|
-
|
196
|
-
pipe.puts('flush tables with read lock;')
|
197
|
-
sleep(@lock_wait_timeout)
|
198
|
-
|
199
|
-
if (thread_id = waiting_read_lock_thread) != ''
|
200
|
-
Process.kill('TERM', @read_lock_pid)
|
201
|
-
|
202
|
-
# after killing the process the mysql thread is still hanging out, need to kill it directly
|
203
|
-
kill_thread_cmd = "mysql -u #{@username} -p#{@password} -e'kill #{thread_id};'"
|
204
|
-
system(kill_thread_cmd)
|
205
|
-
abort "Read lock not acquired after #{@lock_wait_timeout} second timeout. Killed request and aborting backup."
|
206
|
-
end
|
207
|
-
|
208
|
-
true
|
209
|
-
end
|
210
|
-
|
211
|
-
def unlock_tables
|
212
|
-
# technically we don't actually have to do anything here since the spawned
|
213
|
-
# process that has the read lock will die with this one but it doesn't hurt
|
214
|
-
# to be safe
|
215
|
-
Process.kill('TERM', @read_lock_pid)
|
216
|
-
true
|
217
|
-
end
|
218
139
|
|
219
|
-
def
|
220
|
-
@
|
140
|
+
def say(msg, newline = true)
|
141
|
+
return if @opts[:quiet]
|
142
|
+
print("#{msg}#{"\n" if newline}")
|
221
143
|
end
|
222
144
|
end
|
223
145
|
end
|