ey_cloud_server 1.4.5 → 1.4.26
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/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
|