ey_cloud_server 1.3.1 → 1.4.5.pre

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.
@@ -13,20 +13,20 @@ module EY
13
13
  opts.banner = "Usage: ey-snapshots [-flag] [argument]"
14
14
  opts.define_head "ey-snapshots: managing your snapshots..."
15
15
  opts.separator '*'*80
16
-
17
- opts.on("-l", "--list-snapshots", "list snapshots") do
16
+
17
+ opts.on("-l", "--list-snapshots", "list snapshots") do
18
18
  options[:command] = :list_snapshots
19
19
  end
20
20
 
21
21
  opts.on("-c", "--config CONFIG", "Use config file.") do |config|
22
22
  options[:config] = config
23
23
  end
24
-
24
+
25
25
  opts.on("-i", "--instance-id ID", "specify the instance id to work with(only needed if you are running this from ourside of ec2)") do |iid|
26
26
  options[:instance_id] = iid
27
27
  end
28
-
29
-
28
+
29
+
30
30
  opts.on("--snapshot", "take snapshots of both of your volumes(only runs on your ec2 instance)") do
31
31
  options[:command] = :snapshot_volumes
32
32
  end
@@ -34,7 +34,7 @@ module EY
34
34
  opts.on("-q", "--quiet", "Supress output to STDOUT") do
35
35
  options[:quiet] = true
36
36
  end
37
-
37
+
38
38
  end
39
39
 
40
40
  opts.parse!(args)
@@ -58,7 +58,7 @@ module EY
58
58
  get_instance_id
59
59
  silence_stream($stderr) { find_volume_ids }
60
60
  end
61
-
61
+
62
62
  def find_volume_ids
63
63
  @volume_ids = {}
64
64
  @ec2.describe_volumes.each do |volume|
@@ -67,26 +67,26 @@ module EY
67
67
  @volume_ids[:data] = volume[:aws_id]
68
68
  elsif volume[:aws_device] == "/dev/sdz2"
69
69
  @volume_ids[:db] = volume[:aws_id]
70
- end
70
+ end
71
71
  end
72
72
  end
73
73
  say "Volume IDs are #{@volume_ids.inspect}"
74
74
  @volume_ids
75
75
  end
76
-
76
+
77
77
  def list_snapshots
78
78
  @snapshot_ids = {}
79
79
  @ec2.describe_snapshots.sort { |a,b| b[:aws_started_at] <=> a[:aws_started_at] }.each do |snapshot|
80
80
  @volume_ids.each do |mnt, vol|
81
81
  if snapshot[:aws_volume_id] == vol
82
82
  (@snapshot_ids[mnt] ||= []) << snapshot[:aws_id]
83
- end
83
+ end
84
84
  end
85
85
  end
86
86
  say "Snapshots #{@snapshot_ids.inspect}"
87
87
  @snapshot_ids
88
88
  end
89
-
89
+
90
90
  def clean_snapshots(keep=5)
91
91
  list_snapshots
92
92
  @snapshot_ids.each do |mnt, ids|
@@ -101,7 +101,7 @@ module EY
101
101
  end
102
102
  list_snapshots
103
103
  end
104
-
104
+
105
105
  def snapshot_volumes
106
106
  snaps = []
107
107
  @volume_ids.each do |vol, vid|
@@ -120,10 +120,10 @@ module EY
120
120
  end
121
121
  snaps
122
122
  end
123
-
123
+
124
124
  def get_instance_id
125
125
  return @instance_id if @instance_id
126
-
126
+
127
127
  open('http://169.254.169.254/latest/meta-data/instance-id') do |f|
128
128
  @instance_id = f.gets
129
129
  end
@@ -131,14 +131,22 @@ module EY
131
131
  say "Instance ID is #{@instance_id}"
132
132
  @instance_id
133
133
  end
134
-
134
+
135
135
  def sync_filesystem_buffers
136
136
  sync_cmd = "sync && sync && sync"
137
137
  system(sync_cmd)
138
138
  end
139
-
139
+
140
140
  def create_snapshot(volume_id)
141
- snap = @ec2.create_snapshot(volume_id)
141
+ retries = 0
142
+ begin
143
+ snap = @ec2.create_snapshot(volume_id)
144
+ rescue RightAws::AwsError
145
+ retries += 1
146
+ raise if retries > 10
147
+ sleep retries * retries
148
+ retry
149
+ end
142
150
  say "Created snapshot of #{volume_id} as #{snap[:aws_id]}"
143
151
  snap
144
152
  end
@@ -158,11 +166,11 @@ module EY
158
166
  stream.reopen(old_stream)
159
167
  end
160
168
  end
161
-
169
+
162
170
  class Mysql
163
-
171
+
164
172
  attr_accessor :dbh
165
-
173
+
166
174
  def initialize(username, password, lock_wait_timeout)
167
175
  @username = username
168
176
  @password = password
@@ -170,36 +178,36 @@ module EY
170
178
  @lock_wait_timeout = lock_wait_timeout.nil? ? 5 : lock_wait_timeout
171
179
  @master_status_file = "/db/mysql/.snapshot_backup_master_status.txt"
172
180
  end
173
-
181
+
174
182
  def waiting_read_lock_thread
175
- thread_cmd = "mysql -p#{@password} -u #{@username} -N -e 'show full processlist;' | grep 'flush tables with read lock' | awk '{print $1}'"
183
+ thread_cmd = "mysql -p#{@password} -u #{@username} -N -e 'show full processlist;' | grep 'flush tables with read lock' | awk '{print $1}'"
176
184
  %x{#{thread_cmd}}
177
185
  end
178
-
186
+
179
187
  def write_master_status
180
188
  master_status_cmd = "mysql -p#{@password} -u #{@username} -e'SHOW MASTER STATUS\\G' > #{@master_status_file}"
181
189
  system(master_status_cmd)
182
190
  end
183
-
191
+
184
192
  def flush_tables_with_read_lock
185
193
  pipe = IO.popen("mysql -u #{@username} -p#{@password}", 'w')
186
194
  @read_lock_pid = pipe.pid
187
-
195
+
188
196
  pipe.puts('flush tables with read lock;')
189
197
  sleep(@lock_wait_timeout)
190
-
198
+
191
199
  if (thread_id = waiting_read_lock_thread) != ''
192
200
  Process.kill('TERM', @read_lock_pid)
193
-
201
+
194
202
  # after killing the process the mysql thread is still hanging out, need to kill it directly
195
- kill_thread_cmd = "mysql -u #{@username} -p#{@password} -e'kill #{thread_id};'"
203
+ kill_thread_cmd = "mysql -u #{@username} -p#{@password} -e'kill #{thread_id};'"
196
204
  system(kill_thread_cmd)
197
205
  abort "Read lock not acquired after #{@lock_wait_timeout} second timeout. Killed request and aborting backup."
198
206
  end
199
-
207
+
200
208
  true
201
209
  end
202
-
210
+
203
211
  def unlock_tables
204
212
  # technically we don't actually have to do anything here since the spawned
205
213
  # process that has the read lock will die with this one but it doesn't hurt
@@ -207,7 +215,7 @@ module EY
207
215
  Process.kill('TERM', @read_lock_pid)
208
216
  true
209
217
  end
210
-
218
+
211
219
  def disconnect
212
220
  @dbh.disconnect
213
221
  end
@@ -1,3 +1,3 @@
1
1
  module EY::CloudServer
2
- VERSION = '1.3.1'
2
+ VERSION = '1.4.5.pre'
3
3
  end
@@ -0,0 +1,96 @@
1
+ require File.expand_path(__FILE__ + '/../ey-flex')
2
+ require 'logger'
3
+ require 'forwardable'
4
+ require 'stringio'
5
+ require 'open4'
6
+ require 'zlib'
7
+ require 'ostruct'
8
+ require 'fileutils'
9
+
10
+ module EY
11
+ module Backup
12
+ extend self
13
+ attr_accessor :logger, :bucket_minder, :engine, :config
14
+
15
+ def run(argv = ARGV)
16
+ options = CLI.run(argv)
17
+ setup(options)
18
+ dispatch(options)
19
+ end
20
+
21
+ def new_backup(options = {})
22
+ Dumper.run(options)
23
+ end
24
+
25
+ def list(options = {})
26
+ if options[:db].nil? || options[:db].empty?
27
+ BackupSet.list_all
28
+ else
29
+ #list a single database's backups
30
+ BackupSet.list(options[:db])
31
+ end
32
+ end
33
+
34
+ def restore(options = {})
35
+ Loader.run(options)
36
+ end
37
+
38
+ def download(options = {})
39
+ Loader.download(options)
40
+ end
41
+
42
+ def setup(options)
43
+ setup_config(options)
44
+ setup_logger(options)
45
+ setup_minder(options)
46
+ setup_engine(options)
47
+ end
48
+
49
+ def setup_config(options)
50
+ @config = OpenStruct.new(options)
51
+ end
52
+
53
+ def setup_logger(options)
54
+ if options[:quiet]
55
+ @logger ||= Logger.quiet
56
+ else
57
+ @logger ||= Logger.new
58
+ end
59
+ end
60
+
61
+ def setup_minder(options)
62
+ @bucket = "ey-backup-#{Digest::SHA1.hexdigest(options[:aws_secret_id])[0..11]}"
63
+ @bucket_minder = EY::BucketMinder.new(options.merge(:bucket => @bucket))
64
+ end
65
+
66
+ def setup_engine(options)
67
+ @engine = Engine.lookup(options[:engine]).new(options[:dbuser], options[:dbpass])
68
+ end
69
+
70
+ def dispatch(options)
71
+ send(options[:command], options)
72
+ end
73
+
74
+ def base_path
75
+ "/mnt/tmp"
76
+ end
77
+ end
78
+ end
79
+
80
+
81
+ lib_dir = File.expand_path(__FILE__ + '/../ey_backup')
82
+ require lib_dir + '/spawner'
83
+ require lib_dir + '/logger'
84
+ require lib_dir + '/base'
85
+ require lib_dir + '/backup_set'
86
+ require lib_dir + '/cli'
87
+ require lib_dir + '/dumper'
88
+ require lib_dir + '/engine'
89
+ require lib_dir + '/loader'
90
+
91
+ require lib_dir + '/engines/mysql_engine'
92
+ require lib_dir + '/engines/postgresql_engine'
93
+
94
+ require lib_dir + '/processors/gzipper'
95
+ require lib_dir + '/processors/splitter'
96
+ require lib_dir + '/processors/gpg_encryptor'
@@ -0,0 +1,128 @@
1
+ module EY
2
+ module Backup
3
+ class BackupSet < Struct.new(:name, :keys)
4
+ include EY::Backup::Logging
5
+ extend EY::Backup::Logging
6
+
7
+ attr_accessor :files
8
+
9
+ MULTIPART_EXTENSION = /\.part\d+/
10
+
11
+ def self.parse(objects)
12
+ objects.map {|o| new(o[:name], o[:keys]) }
13
+ end
14
+
15
+ def self.from(database, file)
16
+ backup = new(File.basename(file), [])
17
+ backup.files = [file]
18
+ backup
19
+ end
20
+
21
+ def self.cleanup(database, keep)
22
+ say "Cleanup for #{database}"
23
+ backups_for(database)[0...(-keep)].each do |backup|
24
+ say "deleting: #{backup.name}"
25
+ backup.delete!
26
+ end
27
+ end
28
+
29
+ def self.list_all
30
+ list('all', backups)
31
+ end
32
+
33
+ def self.backups
34
+ EY::Backup.config.databases.map {|db| backups_for(db) }.flatten
35
+ end
36
+
37
+ def self.list(database, backups = backups_for(database))
38
+ info "Listing database backups for #{database}"
39
+
40
+ info "#{backups.size} backup(s) found"
41
+
42
+ backups.each_with_index do |db, i|
43
+ info "#{i}:#{database} #{db.normalized_name}"
44
+ end
45
+
46
+ backups
47
+ end
48
+
49
+ def self.backups_for(database_name)
50
+ s3_objects = EY::Backup.bucket_minder.list(:prefix => "#{EY::Backup.config.environment}.#{database_name}", :print => false)
51
+ parse(s3_objects).sort
52
+ end
53
+
54
+ def self.upload(database, file)
55
+ EY::Backup.bucket_minder.name = "#{EY::Backup.config.environment}.#{database}/#{File.basename(file)}"
56
+ EY::Backup.bucket_minder.upload_object(file, false)
57
+ end
58
+
59
+ def self.download(database, index)
60
+ fatal "You didn't specify a database name: e.g. 1:rails_production" if database.empty? || index.empty?
61
+
62
+ backup = backups_for(database)[index.to_i]
63
+
64
+ fatal "No backup found for database #{database}: requested index: #{index}" unless backup
65
+
66
+ backup.download!
67
+
68
+ backup
69
+ end
70
+
71
+ def initialize(*)
72
+ super
73
+
74
+ @files = []
75
+ end
76
+
77
+ def parts
78
+ [keys.size, files.size].max
79
+ end
80
+
81
+ def normalized_name
82
+ normalize(name)
83
+ end
84
+
85
+ def upload!(database)
86
+ files.each do |file|
87
+ EY::Backup.bucket_minder.name = "#{EY::Backup.config.environment}.#{database}/#{File.basename(file)}"
88
+ EY::Backup.bucket_minder.upload_object(file, false)
89
+ info "successful upload: #{file}"
90
+ end
91
+ end
92
+
93
+ def download!
94
+ keys.each do |key|
95
+ info "Downloading #{key}"
96
+ filename = File.join(EY::Backup.base_path, normalize(key))
97
+ File.open(filename, 'wb') do |f|
98
+ EY::Backup.bucket_minder.stream(key) do |chunk|
99
+ f << chunk
100
+ end
101
+ end
102
+
103
+ files << filename
104
+ end
105
+ end
106
+
107
+ def delete!
108
+ keys.each do |key|
109
+ EY::Backup.bucket_minder.remove_object(key)
110
+ end
111
+ end
112
+
113
+ def rm!
114
+ files.each do |file|
115
+ FileUtils.rm(file)
116
+ end
117
+ end
118
+
119
+ def normalize(o)
120
+ o.gsub(/^.*?\//, '')
121
+ end
122
+
123
+ def <=>(o)
124
+ self.name <=> o.name
125
+ end
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,18 @@
1
+ module EY
2
+ module Backup
3
+ class Base
4
+ extend Forwardable
5
+
6
+ def_delegators :logger, :fatal, :error, :warn, :info, :debug, :say
7
+
8
+ def initialize(options = {})
9
+ @environment = options[:env]
10
+ @engine_name = options[:engine]
11
+ end
12
+
13
+ def logger
14
+ EY::Backup.logger
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,89 @@
1
+ module EY
2
+ module Backup
3
+ module CLI
4
+ extend self
5
+
6
+ def run(argv)
7
+ options = default_options.merge(opt_parse(argv))
8
+
9
+ config_path = options[:config] || "/etc/.#{options[:engine]}.backups.yml"
10
+
11
+ config_for(config_path).merge(options)
12
+ end
13
+
14
+ def default_options
15
+ {
16
+ :command => :new_backup,
17
+ :format => "gzip",
18
+ :engine => 'mysql',
19
+ }
20
+ end
21
+
22
+ def opt_parse(argv)
23
+ # Build a parser for the command line arguments
24
+ options = {}
25
+
26
+ opts = OptionParser.new do |opts|
27
+ opts.version = EY::CloudServer::VERSION
28
+
29
+ opts.banner = "Usage: eybackup [-flag] [argument]"
30
+ opts.define_head "eybackup: backing up your shit since way back when..."
31
+ opts.separator '*'*80
32
+
33
+ opts.on("-l", "--list-backup DATABASE", "List mysql backups for DATABASE") do |db|
34
+ options[:db] = (db || 'all')
35
+ options[:command] = :list
36
+ end
37
+
38
+ opts.on("-n", "--new-backup", "Create new mysql backup") do
39
+ options[:command] = :new_backup
40
+ end
41
+
42
+ opts.on("-c", "--config CONFIG", "Use config file.") do |config|
43
+ options[:config] = config
44
+ end
45
+
46
+ opts.on("-d", "--download BACKUP_INDEX", "download the backup specified by index. Run eybackup -l to get the index.") do |index|
47
+ options[:command] = :download
48
+ options[:index] = index
49
+ end
50
+
51
+ opts.on("-e", "--engine DATABASE_ENGINE", "The database engine. ex: mysql, postgres.") do |engine|
52
+ options[:engine] = engine
53
+ end
54
+
55
+ opts.on("-r", "--restore BACKUP_INDEX", "Download and apply the backup specified by index WARNING! will overwrite the current db with the backup. Run eybackup -l to get the index.") do |index|
56
+ options[:command] = :restore
57
+ options[:index] = index
58
+ end
59
+
60
+ opts.on("-k", "--key KEYID", "Public key ID to use for the backup operation") do |key_id|
61
+ options[:key_id] = key_id
62
+ end
63
+
64
+ opts.on("-q", "--quiet", "Supress output to STDOUT") do
65
+ options[:quiet] = true
66
+ end
67
+
68
+ opts.on("-s", "--split_size INTEGER", "Maximum size of a single backup file before splitting.") do |split_size|
69
+ options[:split_size] = split_size.to_i
70
+ end
71
+ end
72
+
73
+ opts.parse!(argv)
74
+
75
+ options
76
+ end
77
+
78
+ def config_for(filename)
79
+ if File.exist?(filename)
80
+ config = YAML::load(File.read(filename))
81
+ config[:environment] ||= config[:env]
82
+ config
83
+ else
84
+ abort "You need to have a backup file at #{filename}"
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end