ey_cloud_server 1.4.58 → 1.4.60

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  !binary "U0hBMQ==":
3
3
  metadata.gz: !binary |-
4
- ZTRlYTdlMDYxN2UzYzI2MTNjM2Y1NGMzMmFjNmJkMWQzOTVkYWU4Mg==
4
+ MjkxYjFlMDg5ODQwOGNmMmVjZmQyM2VlNDhlNzQ3ZjUwNmY3MzJiYQ==
5
5
  data.tar.gz: !binary |-
6
- MGQ4M2EwYWVhMmY2OGU0ZGUxYmNhNDdkODdiMjBmN2MwNjdlZDRkZA==
6
+ ZWEzZmJhMjQ5Y2I2YmFkZWQ1NDVkYWM1OTM5YmIyYzEyNDhmMmI0Zg==
7
7
  SHA512:
8
8
  metadata.gz: !binary |-
9
- N2M0ZGUzOWJhNDRlYTNiMzIxYzQ4YzcwZDY1MGI0OTY2NTVkZDQxZTA4YmQ2
10
- OGY5NmUwNTg4ZDFiM2ZjMjNlNmExMjBmMWJhNjFhNGUxYTVlYzhmODlhYjc0
11
- M2E2N2M5MWJjZDRlZWE3MGM2MDdkNGJlNDkzMjA2MGIxMDBkZjM=
9
+ OWI2N2I3ZWJjNjlhN2EwZDBlZjlhMWJkZjRjYzc1MWZhNGVkZDViZDVjYWEz
10
+ NWUxMGI3NzcyM2EyNmVmZDAzYzQ1MGVkZmY2ZDQyNTBlMGYwZjM3OThmMDE0
11
+ YWU5ODA3MDc2NTc4NDYxZTQzYjU3Mjg1MTU4YTM3ZTkwNzNiOWI=
12
12
  data.tar.gz: !binary |-
13
- Mzc1YTczYzU1M2QyODA2NDgwZjE0MzJmNTVmOGEzOGZlNzJjYTc5NGRmYmEz
14
- OWZjNjc0NjEyYWVhM2JkOGQxMmFkZTE0NWViNWQ2MDBlYzVjZGVmZTZkZTc2
15
- NzgwYTI2NjcwZmQ0NWUwOWVmOTlkNTM3NjM2MzMxYjBlMGE4OTI=
13
+ ZTBjZjEzNTUzZGI5NTVlNjc1ZWYwZTliYjNhOWYyNjIyYzkyNzc4MjA4YWI2
14
+ MjJiZDg3ZTI5MTEzZTNlZGUzYTViNTc3Mjc1NDYyYjNlNzAwMjBlMDg5ZGYx
15
+ ZGY4ZTEwNjM5NTc2NjZlMTk5NWYwNDUzOWYwNWFmY2VlMjNjZWY=
data/bin/ey-snapshots CHANGED
@@ -10,6 +10,7 @@ require File.dirname(__FILE__) + '/../lib/ey-flex'
10
10
  begin
11
11
  EY::SnapshotMinder.run(ARGV)
12
12
  rescue => e
13
- EY.notify_snapshot_error(e)
13
+ puts e.message
14
+ EY.notify_snapshot_error(e) unless STDOUT.isatty
14
15
  raise
15
16
  end
data/bin/eybackup CHANGED
@@ -12,7 +12,9 @@ require File.dirname(__FILE__) + '/../lib/ey-flex'
12
12
  begin
13
13
  EY::Backup.run(ARGV)
14
14
  rescue => e
15
- EY.notify_backup_error(e)
16
- raise
15
+ puts e.message
16
+ puts "******* Trace *******"
17
+ puts e.inspect
18
+ EY.notify_backup_error(e) unless STDOUT.isatty
17
19
  end
18
20
 
data/bin/eyrestore CHANGED
@@ -7,7 +7,7 @@ require 'timeout'
7
7
 
8
8
  def opt_parse(argv)
9
9
  options = {}
10
- optparse = OptionParser.new do |opts|
10
+ @optparse = OptionParser.new do |opts|
11
11
  opts.banner = "Usage: #{__FILE__} --env <environment name> --database <database_name> --action <action> [options]"
12
12
  opts.separator ''
13
13
  opts.separator "Wrapper for eybackup used for listing, downloading and restoring database backups. The main purpose for this tool is to"
@@ -20,7 +20,7 @@ def opt_parse(argv)
20
20
  opts.separator 'Common Options:'
21
21
 
22
22
  opts.on('-e', '--env environment_name', "Specifies the source environment name for the backups you will be working with.") { |env| options[:env] = env }
23
- opts.on('-d', '--database database_name', "Name of the source database to get backups for.") { |dbname| options[:databases] = [dbname] }
23
+ opts.on('-d', '--database database_name', "Name of the source database to list backups for.") { |dbname| options[:databases] = [dbname] }
24
24
  actions=['list', 'restore', 'download']
25
25
  opts.on('-a', '--action action_name', "The action you want to perform using the backup #{actions}.") do |action|
26
26
  unless actions.include?(action)
@@ -65,7 +65,7 @@ def opt_parse(argv)
65
65
  opts.separator " sudo -i #{__FILE__} --env production --database todo --action restore --index last"
66
66
  opts.separator ''
67
67
  end
68
- optparse.parse!
68
+ @optparse.parse!
69
69
  options
70
70
  end
71
71
 
@@ -83,6 +83,7 @@ end
83
83
 
84
84
  def extra_validations(options)
85
85
  mandatory = [:databases, :env, :action]
86
+ mandatory << :index if %w(download restore).include? options[:action]
86
87
  missing = mandatory.select{ |param| options[param].nil? }
87
88
  raise OptionParser::MissingArgument, missing.join(',') unless missing.empty?
88
89
  end
@@ -92,7 +93,9 @@ def list_backups(engine, db, path)
92
93
  debug("Listing backups with command: #{command}")
93
94
  res = %x{#{command}}
94
95
  last = res.chomp.split("\n").last
95
- abort "No Backups Found for that environment and database." if last.match(/0 backup\(s\) found/)
96
+ if last.match(/0 backup\(s\) found/)
97
+ abort "No Backups Found for that environment and database.\n Tips:\n - double check the source environment name is correct\n - you may have a legacy bucket (grep backup_bucket /etc/.*.backups.yml) in the source environment, pass to eyrestore with --backup_bucket"
98
+ end
96
99
  res
97
100
  end
98
101
 
@@ -104,6 +107,14 @@ rescue Timeout::Error
104
107
  '' # return nil if timeout
105
108
  end
106
109
 
110
+ def backup_idx(listing, index)
111
+ if index == 'last'
112
+ last_backup_idx(listing)
113
+ else
114
+ index
115
+ end
116
+ end
117
+
107
118
  def last_backup_idx(listing)
108
119
  last = listing.chomp.split("\n").last
109
120
  if last.split.first.match(/\d+:\w+/)
@@ -132,7 +143,15 @@ config.merge!(eyrestore) unless eyrestore.nil?
132
143
 
133
144
  # override config from command line options
134
145
  config.merge!(options)
135
- extra_validations(config)
146
+ indexdb = config[:index].split(':')[1] if not config[:index].nil? and config[:index].include? ':'
147
+ config[:databases] = [indexdb] if config[:databases].nil? and not indexdb.nil? and indexdb.match(/^\w+$/)
148
+ begin
149
+ extra_validations(config)
150
+ rescue OptionParser::MissingArgument => e
151
+ puts e.message
152
+ puts @optparse
153
+ exit
154
+ end
136
155
  debug("Config Options: '#{config}'")
137
156
 
138
157
  # create a temporary configuration file from ~/.eyrestore.config.yml
@@ -156,8 +175,10 @@ end
156
175
 
157
176
  if ['download','restore'].include?(config[:action])
158
177
  config[:index] = gets_timeout("Enter the backup index to use for #{config[:action]} (e.g. 9:#{db}) ", 30) unless config[:index]
159
- config[:index] = last_backup_idx(list_backups(engine, db, temp)) if config[:index] == 'last'
160
- abort "Invalid backup index '#{config[:index]}'." unless config[:index].match(/^\d+:\w+$/)
178
+ config[:index] = backup_idx(list_backups(engine, db, temp), config[:index])
179
+ # config[:index] = last_backup_idx(list_backups(engine, db, temp)) if config[:index] == 'last'
180
+ config[:index] = config[:index] + ":#{config[:databases].first}" if config[:index].match(/^\d+$/) and config[:databases] and config[:databases].size == 1
181
+ abort "Invalid backup index '#{config[:index]}'; proper format is <number>:<dbname>." unless config[:index].match(/^\d+:\w+$/)
161
182
 
162
183
  action = config[:action] == 'restore' ? '-r' : '-d'
163
184
  debug("Set action flag for eybackup to '#{action}' based on action of '#{config[:action]}'")
@@ -174,7 +195,15 @@ if ['download','restore'].include?(config[:action])
174
195
 
175
196
  res=%x{#{command}}
176
197
  puts res
177
- puts "Restore complete!" if res.split("\n").last.match(/^Filename/) and config[:action] == 'restore'
198
+
199
+ last_line = res.split("\n").last
200
+ if last_line.nil?
201
+ # No-op, eybackup messaging is adequate.
202
+ elsif last_line.match(/^Filename/) and config[:action] == 'restore' and not last_line.match(/.gpz$/)
203
+ puts "Restore complete!"
204
+ else
205
+ puts "Download complete!"
206
+ end
178
207
  end
179
208
 
180
209
  # cleanup temporary config file
@@ -16,6 +16,10 @@ module EY
16
16
  @bucket ||= @s3.directories.get(@bucket_name)
17
17
  end
18
18
 
19
+ def file
20
+ bucket
21
+ end
22
+
19
23
  def files
20
24
  bucket.files
21
25
  end
@@ -74,7 +78,13 @@ module EY
74
78
  end
75
79
 
76
80
  def put(filename, contents)
77
- files.create(:key => filename, :body => contents)
81
+ files.create(
82
+ :key => filename,
83
+ :body => contents,
84
+ :public => false,
85
+ :multipart_chunk_size => 100*1024*1024, # 100MB
86
+ 'x-amz-server-side-encryption' => 'AES256'
87
+ )
78
88
  end
79
89
  end
80
90
  end
data/lib/ey-flex.rb CHANGED
@@ -25,7 +25,7 @@ module EY
25
25
  end
26
26
 
27
27
  def self.enzyme_api
28
- @enzyme_api ||= EY::Enzyme::API.new(
28
+ @enzyme_api ||= EY::Enzyme::API::Client.new(
29
29
  enzyme_config[:api],
30
30
  enzyme_config[:instance_id],
31
31
  enzyme_config[:token],
@@ -8,7 +8,6 @@ module EY
8
8
 
9
9
  def initialize(secret_id, secret_key, region, bucket_name)
10
10
  @bucket_minder = EY::BucketMinder.new(secret_id, secret_key, bucket_name, region)
11
- @s3 = Fog::Storage.new(:provider => 'AWS', :aws_access_key_id => secret_id, :aws_secret_access_key => secret_key, :region => region)
12
11
  end
13
12
  attr_reader :bucket_minder
14
13
 
@@ -17,13 +16,13 @@ module EY
17
16
  begin
18
17
  object_name = File.join("#{environment_name}.#{database_name}", "#{File.basename(filename)}")
19
18
  info "Starting upload: #{filename}"
20
- @s3.put_object(@bucket_minder.bucket_name, object_name, File.open(filename,'r'))
19
+ @bucket_minder.put(object_name, File.open(filename, 'r'))
21
20
  info "Successful upload: #{filename}"
22
21
  rescue => e
23
22
  retries ||= 5
24
23
  retries -= 1
25
24
  # remove partial or failed uploads
26
- @s3.delete_object(@bucket_minder.bucket_name, object_name) rescue nil
25
+ @bucket_minder.remove_object(object_name)
27
26
  raise e if retries == 0
28
27
  warn "retrying upload of #{filename}. Got: #{e.inspect}"
29
28
  retry
@@ -31,26 +31,26 @@ module EY
31
31
  end
32
32
 
33
33
  def self.list(databases)
34
- names = databases.map do |db|
35
- db.name
36
- end
37
- info "Listing database backups for #{names.join(', ')}"
38
-
39
- backups = databases.map{ |db| db.backups }.flatten
40
-
41
- puts "#{backups.size} backup(s) found"
42
-
43
- backups.each_with_index do |backup_set, i|
44
- puts "#{i}:#{backup_set.database_name} #{backup_set.normalized_name}"
34
+ all_backups = []
35
+ databases.each do |db|
36
+ backups = db.backups
37
+ all_backups << backups
38
+ puts "Listing database backups for #{db.name}"
39
+ puts "#{backups.size} backup(s) found"
40
+
41
+ backups.each_with_index do |backup_set, i|
42
+ puts "#{i}:#{backup_set.database_name} #{backup_set.normalized_name}"
43
+ end
44
+ puts # just some extra whitespace
45
45
  end
46
-
47
- backups
46
+
47
+ all_backups.flatten
48
48
  end
49
49
 
50
50
  def self.download(database, index)
51
- fatal "You didn't specify a database name: e.g. 1:rails_production" if database.nil? || index.empty?
51
+ fatal "You didn't specify a database name: e.g. 1:rails_production" if database.nil? || index < 0
52
52
 
53
- backup = database.backups[index.to_i]
53
+ backup = database.backups[index]
54
54
 
55
55
  fatal "No backup found for database #{database}: requested index: #{index}" unless backup
56
56
 
@@ -78,9 +78,9 @@ module EY
78
78
  def download!
79
79
  @remote_filenames = []
80
80
  @keys.each do |key|
81
- info "Downloading #{key} to #{@database.base_path}"
82
81
  remote_filename = File.join(@database.base_path, normalize(key))
83
82
  puts "Filename: #{remote_filename}"
83
+ info "Downloading #{key} to #{@database.base_path}"
84
84
  File.open(remote_filename, 'wb') do |f|
85
85
  @database.bucket_minder.stream(key) do |chunk, remaining_size, total_size|
86
86
  f.write(chunk)
data/lib/ey_backup/cli.rb CHANGED
@@ -34,14 +34,16 @@ module EY
34
34
  # Build a parser for the command line arguments
35
35
  options = {}
36
36
 
37
- opts = OptionParser.new do |opts|
37
+ optparse = OptionParser.new do |opts|
38
38
  opts.version = EY::CloudServer::VERSION
39
39
 
40
40
  opts.banner = "\nUsage: eybackup [-flag] [argument]"
41
- opts.define_head " eybackup: manage dump (mysqldump/pg_dump) style backups of your database."
41
+ opts.define_head " eybackup: manage logical (mysqldump/pg_dump) style backups of your database.",
42
+ " When backing up multiple databases, each database is backed up separately."
42
43
  opts.separator '*'*80
43
44
 
44
- opts.on("-l", "--list-backup DATABASE", "List backups for DATABASE") do |db|
45
+ opts.on("-l", "--list-backup DATABASE_NAME", "List backups for DATABASE_NAME; ",
46
+ " accepts 'all' to list all databases in the config (/etc/.*.backups.yml)") do |db|
45
47
  if db == "all"
46
48
  db = nil
47
49
  end
@@ -52,7 +54,9 @@ module EY
52
54
  opts.on("-e", "--engine DATABASE_ENGINE", "The database engine. ex: mysql, postgresql.") do |engine|
53
55
  options[:engine] = engine
54
56
  end
55
- opts.on("-n", "--new-backup", "Create a new backup (default)") do
57
+ opts.on("-n", "--new-backup [DATABASE_NAME]", "Create a new backup (default).",
58
+ " Backs up each database in '/etc/.*.backups.yml' if DATABASE_NAME not set.") do |db|
59
+ options[:db] = db.split(',') unless db.nil? or db == ''
56
60
  options[:command] = :new_backup
57
61
  end
58
62
 
@@ -89,9 +93,11 @@ module EY
89
93
  ' Run `eybackup -l #{db_name}` to get the index.',
90
94
  ' BACKUP_INDEX uses the format #{index_number}:#{db_name}') do |index_and_db|
91
95
  options[:command] = :download
92
- db, index = split_index(index_and_db)
96
+ index, db = split_index(index_and_db)
97
+ index = index.to_i
93
98
  options[:index] = index
94
99
  options[:db] = db
100
+ raise OptionParser::InvalidArgument, "Index '#{index_and_db}' is not a valid format (hint: <number>:<dbname>)!" if index.nil? or not index.is_a? Numeric
95
101
  end
96
102
 
97
103
  opts.on("-r", "--restore BACKUP_INDEX", "Download and apply the backup specified by index.",
@@ -99,9 +105,11 @@ module EY
99
105
  ' Run `eybackup -l #{db_name}` to get the index.',
100
106
  ' BACKUP_INDEX uses the format #{index_number}:#{db_name}') do |index_and_db|
101
107
  options[:command] = :restore
102
- db, index = split_index(index_and_db)
108
+ index, db = split_index(index_and_db)
109
+ index = index.to_i
103
110
  options[:index] = index
104
111
  options[:db] = db
112
+ raise OptionParser::InvalidArgument, "Index '#{index_and_db}' is not a valid format (hint: <number>:<dbname>)!" if index.nil? or not index.is_a? Numeric
105
113
  end
106
114
 
107
115
  options[:force] = false
@@ -109,16 +117,31 @@ module EY
109
117
  " For use with automated restore operations (e.g. Staging).") do
110
118
  options[:force] = true
111
119
  end
120
+
121
+ options[:allow_concurrent] = false
122
+ opts.on("--allow_concurrent", "Allow eybackup process to run concurrently with other eybackup processes.") do
123
+ options[:allow_concurrent] = true
124
+ end
125
+
126
+ options[:skip_analyze] = false
127
+ opts.on("--skip_analyze", "Skip automatic analyze during PostgreSQL Restore operations.") do
128
+ options[:skip_analyze] = true
129
+ end
112
130
 
113
131
  end
114
132
 
115
- opts.parse!(argv)
133
+ begin
134
+ optparse.parse!(argv)
135
+ rescue => e
136
+ puts optparse
137
+ raise
138
+ end
116
139
 
117
140
  options
118
141
  end
119
142
 
120
143
  def split_index(index)
121
- index.split(':').reverse
144
+ index.split(':')
122
145
  end
123
146
 
124
147
  def config_for(filename)
@@ -1,3 +1,9 @@
1
+ class String
2
+ def truncate(limit = 1)
3
+ self.match(%r{^(.{0,#{limit}})})[1]
4
+ end
5
+ end
6
+
1
7
  module EY
2
8
  module Backup
3
9
  class Dumper < Base
@@ -26,15 +32,19 @@ module EY
26
32
 
27
33
  alert_level=exceptions.empty? ? 'OKAY' : 'FAILURE'
28
34
 
29
- message.gsub!("\n", '\n')
30
- full_txt= "Severity: #{alert_level}\n" \
31
- + "Time: #{Time.now.to_i}\n" \
32
- + "Type: process-dbbackup summary\n" \
33
- + "Plugin: exec\n" \
34
- + "raw_message: '#{message}'"
35
- full_txt = Shellwords.escape(full_txt)
36
- alert_command = %Q(echo #{full_txt} | /engineyard/bin/ey-alert.rb 2>&1)
37
- system(alert_command)
35
+ # we don't provide alert here unless its a failure
36
+ unless alert_level == 'OKAY'
37
+ message.gsub!("\n", '\n')
38
+ message = Shellwords.escape(message).truncate(255)
39
+ full_txt= "Severity: #{alert_level}\n" \
40
+ + "Time: #{Time.now.to_i}\n" \
41
+ + "Type: process-dbbackup summary\n" \
42
+ + "Plugin: exec\n"
43
+ full_txt = Shellwords.escape(full_txt)
44
+ full_txt += "raw_message:\\ \\'#{message}\\'"
45
+ alert_command = %Q(echo #{full_txt} | /engineyard/bin/ey-alert.rb 2>&1)
46
+ system(alert_command)
47
+ end
38
48
  end
39
49
 
40
50
  def initialize(database)
@@ -48,7 +58,7 @@ module EY
48
58
  backup_set.split!(split_size)
49
59
  backup_set.upload!
50
60
 
51
- okay("Successful backup: #{@database.name} (#{@database.backup_size})", @database.name)
61
+ okay(@database.name, @database.backup_size)
52
62
  backup_set.cleanup
53
63
  backup_set.rm!
54
64
  @database.backup_size
@@ -3,7 +3,7 @@ module EY
3
3
  class Engine < Base
4
4
  include Spawner
5
5
 
6
- attr_reader :username, :password, :host, :key_id, :force
6
+ attr_reader :username, :password, :host, :key_id, :force, :allow_concurrent, :skip_analyze
7
7
 
8
8
  def self.label
9
9
  @label
@@ -26,8 +26,21 @@ module EY
26
26
  EY::Backup.logger.fatal("Unknown database engine: #{label}")
27
27
  end
28
28
 
29
- def initialize(username, password, host, key_id, force)
30
- @username, @password, @host, @key_id, @force = username, password, host, key_id, force
29
+ def initialize(username, password, host, key_id, force, allow_concurrent, skip_analyze)
30
+ @username, @password, @host, @key_id, @force, @allow_concurrent, @skip_analyze = username, password, host, key_id, force, allow_concurrent, skip_analyze
31
+ end
32
+
33
+ def block_concurrent(db = nil)
34
+ if backup_running?
35
+ message = "Unable to backup #{db}: already a backup in progress. Use --allow_concurrent to enable concurrent backup runs."
36
+ error(message, db) unless File.exists?('/etc/engineyard/skip_concurrent_alerts') # this skips the dashboard alert
37
+ abort message
38
+ end
39
+ end
40
+
41
+ def backup_running?
42
+ verbose %x{ps aux | grep [e]ybackup | grep rubygems}
43
+ %x{ps -ef | grep [e]ybackup | grep rubygems | wc -l}.to_i > 1
31
44
  end
32
45
 
33
46
  def gpg?
@@ -6,7 +6,7 @@ module EY
6
6
  def dump(database_name, basename)
7
7
  file = basename + '.sql'
8
8
 
9
- command = "mysqldump #{username_option} #{host_option} #{single_transaction_option(database_name)} #{routines_option} #{master_data_option} #{databases_option(database_name)} 2> /tmp/eybackup.$$.dumperr"
9
+ command = "( #{server_id} mysqldump #{username_option} #{host_option} #{single_transaction_option(database_name)} #{routines_option} #{master_data_option} #{databases_option(database_name)} ) "
10
10
 
11
11
  if gpg?
12
12
  command << " | " << GPGEncryptor.command_for(key_id)
@@ -17,6 +17,8 @@ module EY
17
17
  end
18
18
 
19
19
  command << " > #{file}"
20
+
21
+ block_concurrent(database_name) unless allow_concurrent
20
22
 
21
23
  run(command, database_name)
22
24
 
@@ -27,14 +29,16 @@ module EY
27
29
  command = "cat #{file}"
28
30
 
29
31
  if file =~ /.gpz$/ # GPG?
30
- abort "Cannot restore a GPG backup directly; decrypt the file (#{file}) using your key and then load using the mysql client."
32
+ abort "\nCannot restore a GPG backup directly; decrypt the file (#{file}) using your key and then load using the mysql client.
33
+ To decrypt a backup: https://support.cloud.engineyard.com/hc/en-us/articles/205413948-Use-PGP-Encrypted-Database-Backups-with-Engine-Yard-Cloud#restore
34
+ Once decrypted, restore with: `gunzip -f < <filename> | mysql #{username_option} #{host_option} #{database_name}`\n\n"
31
35
  else
32
36
  command << " | " << GZipper.gunzip
33
37
  end
34
38
 
35
39
  cycle_database(database_name)
36
40
 
37
- command << " | mysql #{username_option} #{host_option} #{database_name} 2> /tmp/eybackup.$$.dumperr"
41
+ command << " | mysql #{username_option} #{host_option} #{database_name} "
38
42
 
39
43
  run(command, database_name)
40
44
  end
@@ -52,12 +56,19 @@ module EY
52
56
  "--routines"
53
57
  end
54
58
 
59
+ def server_id
60
+ if log_bin_on?
61
+ stdout = %x{mysql #{username_option} #{host_option} -BN -e"select @@global.server_id"}
62
+ "echo '-- Server_id: #{stdout.strip}' && "
63
+ end
64
+ end
65
+
55
66
  def master_data_option
56
67
  "--master-data=2" if log_bin_on?
57
68
  end
58
69
 
59
70
  def databases_option(db)
60
- "--add-drop-database --databases #{db}"
71
+ "#{db}"
61
72
  end
62
73
 
63
74
  def host_option
@@ -6,7 +6,7 @@ module EY
6
6
  def dump(database_name, basename)
7
7
  file = basename + '.dump'
8
8
 
9
- command = "PGPASSWORD='#{password}' pg_dump -h #{host} --create --format=c -Upostgres #{database_name} 2> /tmp/eybackup.$$.dumperr"
9
+ command = "PGPASSWORD='#{password}' pg_dump -h #{host} --create --format=c -Upostgres #{database_name} "
10
10
 
11
11
  if gpg?
12
12
  command << " | " << GPGEncryptor.command_for(key_id)
@@ -15,6 +15,7 @@ module EY
15
15
 
16
16
  command << " > #{file}"
17
17
 
18
+ block_concurrent(database_name) unless allow_concurrent
18
19
  run(command, database_name)
19
20
 
20
21
  file
@@ -22,16 +23,24 @@ module EY
22
23
 
23
24
  def load(database_name, file)
24
25
  if file =~ /.gpz$/ # GPG?
25
- abort "Cannot restore a GPG backup directly; decrypt the file (#{file}) using your key and then load with pg_restore."
26
+ abort "\nCannot restore a GPG backup directly; decrypt the file (#{file}) using your key and then load with pg_restore.
27
+ To decrypt a backup: https://support.cloud.engineyard.com/hc/en-us/articles/205413948-Use-PGP-Encrypted-Database-Backups-with-Engine-Yard-Cloud#restore
28
+ Once decrypted, restore with: `pg_restore -h #{host} --format=c --clean -Upostgres -d #{database_name} <filename>`\n\n"
26
29
  end
27
30
 
28
31
  cycle_database(database_name)
29
32
 
30
33
  command = "cat #{file}"
31
34
 
32
- command << " | PGPASSWORD='#{password}' pg_restore -h #{host} --format=c -Upostgres -d #{database_name} 2> /tmp/eybackup.$$.dumperr"
35
+ command << " | PGPASSWORD='#{password}' pg_restore -h #{host} --format=c -Upostgres -d #{database_name}"
33
36
 
34
37
  run(command, database_name)
38
+
39
+ # Analyze database unless disabled
40
+ unless skip_analyze
41
+ verbose "Analyzing database '#{database_name}', use --skip-analyze to skip this step."
42
+ %x{PGPASSWORD='#{password}' vacuumdb -h #{host} -Upostgres -d #{database_name} --analyze-only}
43
+ end
35
44
  end
36
45
 
37
46
  def check_connections(database_name)
@@ -40,7 +49,7 @@ module EY
40
49
  if active_connections.to_i > 0
41
50
  res = ''
42
51
  unless force
43
- puts "There are currently #{stdout.string.to_i} connections on database: '#{database_name}'; can I kill these to continue (Y/n):"
52
+ puts "There are currently #{active_connections} connections on database: '#{database_name}'; can I kill these to continue (Y/n):"
44
53
  Timeout::timeout(30){
45
54
  res = gets.strip
46
55
  }
@@ -16,8 +16,10 @@ module EY
16
16
  end
17
17
 
18
18
  def run
19
- info "Restoring #{@database.name}"
19
+
20
20
  backup = download
21
+ mode = backup.basename =~ /.gpz$/ ? "Downloading" : "Restoring"
22
+ info "#{mode} #{@database.name}"
21
23
  backup.load!
22
24
  backup.remove_joined_file!
23
25
  end
@@ -1,8 +1,15 @@
1
+ class String
2
+ def truncate(limit = 1)
3
+ self.match(%r{^(.{0,#{limit}})})[1]
4
+ end
5
+ end
6
+
1
7
  module EY
2
8
  module Backup
3
9
  class Logger
4
10
  extend Forwardable
5
11
  require 'shellwords'
12
+ require 'fileutils'
6
13
 
7
14
  attr_accessor :stdout, :stderr
8
15
 
@@ -22,21 +29,22 @@ module EY
22
29
 
23
30
  def push_dashboard_alert(message, alert_level, db = nil)
24
31
  message.gsub!("\n", '\n')
32
+ message = Shellwords.escape(message).truncate(255)
25
33
  type="process-dbbackup"
26
34
  type = type + " #{db}" unless db.nil?
27
35
  full_txt= "Severity: #{alert_level}\n" \
28
36
  + "Time: #{Time.now.to_i}\n" \
29
37
  + "Type: #{type}\n" \
30
- + "Plugin: exec\n" \
31
- + "raw_message: '#{message}'"
38
+ + "Plugin: exec\n"
32
39
  full_txt = Shellwords.escape(full_txt)
40
+ full_txt += "raw_message:\\ \\'#{message}\\'"
33
41
  alert_command = %Q(echo #{full_txt} | /engineyard/bin/ey-alert.rb 2>&1)
34
42
  verbose "Sending dashboard alert"
35
43
  system(alert_command)
36
44
  end
37
45
 
38
46
  def info(msg)
39
- puts "#{Time.now} #{msg}"
47
+ stdout.puts "#{Time.now} #{msg}"
40
48
  end
41
49
 
42
50
  def verbose(msg)
@@ -46,24 +54,48 @@ module EY
46
54
  def set_verbose()
47
55
  @verbose = true
48
56
  end
57
+
58
+ def set_log_path=(path)
59
+ @status_path = path
60
+ FileUtils.mkdir_p(@status_path)
61
+ end
49
62
 
50
63
  def say(msg, newline = true)
51
64
  newline ? info(msg) : stdout.print(msg)
52
65
  end
53
66
 
54
- def okay(msg, db = nil)
55
- stderr.puts("#{Time.now} OKAY: #{msg}")
56
- # push_dashboard_alert(msg, "OKAY", db), we don't push OKAY alerts, we push a summary alert instead.
67
+ def okay(db, size)
68
+ filepath = File.join(@status_path, "#{db}.sizes")
69
+ %x{LOG=$(tail -n 100 #{filepath}); echo "$LOG" > #{filepath}} if File.exists?(filepath)
70
+ %x{echo "#{Time.now()} #{size}" >> #{filepath}}
71
+ msg = "Backup successful for '#{db}' after previous failure."
72
+ push_dashboard_alert(msg, 'OKAY', db) if clear_alert?(db)
57
73
  end
58
74
 
59
75
  def warn(msg, db = nil)
60
- stderr.puts("#{Time.now} WARNING: #{msg}")
76
+ stdout.puts("#{Time.now} WARNING: #{msg}")
77
+ set_alert(msg, db) unless db.nil?
61
78
  push_dashboard_alert(msg, "WARNING", db)
62
79
  end
63
80
 
64
81
  def error(msg, db = nil)
65
- stderr.puts("#{Time.now} ERROR: #{msg}")
66
- push_dashboard_alert("#{msg}. Details at /var/log/eybackup.log.", "FAILURE", db)
82
+ stdout.puts("#{Time.now} ERROR: #{msg}")
83
+ set_alert(msg, db) unless db.nil?
84
+ push_dashboard_alert("#{msg} Details at /var/log/eybackup.log.", "FAILURE", db)
85
+ end
86
+
87
+ def set_alert(msg, db)
88
+ fullPath = File.join(@status_path, "#{db}.alert")
89
+ File.open(fullPath, 'a') { |file| file.puts "#{Time.now}: #{msg}"}
90
+ end
91
+
92
+ def clear_alert?(db)
93
+ begin
94
+ File.delete(File.join(@status_path, "#{db}.alert"))
95
+ true
96
+ rescue Errno::ENOENT
97
+ false
98
+ end
67
99
  end
68
100
 
69
101
  def exception(type, msg)
@@ -71,7 +103,7 @@ module EY
71
103
  end
72
104
 
73
105
  def debug(msg)
74
- stderr.puts("#{Time.now} DEBUG: #{msg}")
106
+ stdout.puts("#{Time.now} DEBUG: #{msg}")
75
107
  end
76
108
 
77
109
  end
@@ -5,7 +5,7 @@ module EY
5
5
  include Logging
6
6
 
7
7
  CHUNK_SIZE = 1024 * 64
8
- MAX_FILE_SIZE = (4.5*1024*1024*1024).to_i #4.5GB
8
+ MAX_FILE_SIZE = (4.9*1024*1024*1024*1024).to_i #4.9TB
9
9
 
10
10
  def self.dump(file, split_size)
11
11
  split_size ||= MAX_FILE_SIZE
@@ -42,14 +42,11 @@ module EY
42
42
  pid, stdin, stdout, stderr = Open4.popen4("bash -o pipefail -c #{escaped_command}")
43
43
  pid, status = Process::waitpid2(pid)
44
44
 
45
- verbose "stdout: #{stdout.read}"
46
- verbose "stderr: #{stderr.read}"
47
45
  verbose "status: #{status}"
48
46
 
49
47
  if ! status.success?
50
- dumperr = File.exists?("/tmp/eybackup.#{pid}.dumperr") ? File.read("/tmp/eybackup.#{pid}.dumperr") : status
51
- err_msg = "DB dump failed. The error returned was: #{dumperr}"
52
- verbose "#{db} backup failed: #{err_msg}"
48
+ dumperr = File.exists?("/tmp/eybackup.#{pid}.dumperr") ? File.read("/tmp/eybackup.#{pid}.dumperr") : "#{status}: #{stderr.read.chomp}: #{stdout.read.chomp}"
49
+ err_msg = "#{db} backup failed! The error returned was: #{dumperr}"
53
50
  error(err_msg, db)
54
51
  end
55
52
 
data/lib/ey_backup.rb CHANGED
@@ -44,6 +44,7 @@ module EY
44
44
  class << self
45
45
  attr_accessor :logger
46
46
  attr_accessor :tmp_dir
47
+ attr_accessor :log_dir
47
48
  end
48
49
 
49
50
  def self.run(argv = ARGV)
@@ -96,11 +97,12 @@ module EY
96
97
  if @options[:db].nil? || @options[:db].empty?
97
98
  @options[:databases]
98
99
  else
99
- [@options[:db]]
100
+ [@options[:db]].flatten
100
101
  end
101
102
  end
102
103
 
103
104
  def setup
105
+ setup_log_dir
104
106
  setup_logger
105
107
  setup_tmp_dir
106
108
  setup_backend
@@ -117,6 +119,7 @@ module EY
117
119
  EY::Backup.logger.set_verbose
118
120
  end
119
121
  end
122
+ EY::Backup.logger.set_log_path=EY::Backup.log_dir
120
123
  end
121
124
 
122
125
  def setup_tmp_dir
@@ -126,6 +129,14 @@ module EY
126
129
  EY::Backup.tmp_dir = "/mnt/tmp"
127
130
  end
128
131
  end
132
+
133
+ def setup_log_dir
134
+ if @options[:log_dir]
135
+ EY::Backup.log_dir = @options[:log_dir]
136
+ else
137
+ EY::Backup.log_dir = "/var/log/engineyard/eybackup"
138
+ end
139
+ end
129
140
 
130
141
  def setup_backend
131
142
  @backend = Backend.new(@options[:aws_secret_id], @options[:aws_secret_key], @options[:region], @options[:backup_bucket])
@@ -136,7 +147,7 @@ module EY
136
147
  if ! @options.key?(:dbhost) or @options[:dbhost] == nil or @options[:dbhost] == ""
137
148
  @options[:dbhost] = 'localhost'
138
149
  end
139
- @engine = engine_class.new(@options[:dbuser], @options[:dbpass], @options[:dbhost], @options[:key_id], @options[:force])
150
+ @engine = engine_class.new(@options[:dbuser], @options[:dbpass], @options[:dbhost], @options[:key_id], @options[:force], @options[:allow_concurrent], @options[:skip_analyze])
140
151
  end
141
152
 
142
153
  def dispatch
@@ -1,5 +1,5 @@
1
1
  module EY
2
2
  module CloudServer
3
- VERSION = '1.4.58'
3
+ VERSION = '1.4.60'
4
4
  end
5
5
  end
data/spec/config.yml CHANGED
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  tmp_dir:
3
+ log_dir:
3
4
  data_json:
4
5
  mysql_user: root
5
6
  mysql_password:
@@ -125,6 +125,24 @@ describe "MySQL Backups" do
125
125
  run_sql("SELECT * FROM `bar`;", @db_name).should be_true
126
126
  FileUtils.rm(file)
127
127
  end
128
+
129
+ it 'detects a backup failure due to an invalid view definition and reports it to stdout' do
130
+
131
+ run_sql("CREATE TABLE `foo` (`id` int(11) NOT NULL auto_increment, PRIMARY KEY(`id`));", @db_name).should be_truthy
132
+ run_sql("CREATE TABLE `bar` (`id` int(11) NOT NULL auto_increment, PRIMARY KEY(`id`));", @db_name).should be_truthy
133
+ run_sql("CREATE view `vw_foobar` as select `foo`.`id` as `foo_id`, `bar`.`id` as `bar_id` from `foo` inner join `bar` on `foo`.`id` = `bar`.`id`;", @db_name).should be_truthy
134
+ run_sql("DROP TABLE `bar`;", @db_name).should be_truthy
135
+
136
+ reset_logger
137
+
138
+ EY::Backup.run(["-c", backup_config_file])
139
+ stdout.should match(/mysqldump: Couldn't execute 'SHOW FIELDS FROM `vw_foobar`'/)
140
+
141
+ files = Dir["#{EY::Backup.tmp_dir}/*#{@db_name}*"]
142
+
143
+ files.size.should == 1
144
+ FileUtils.rm(files.first)
145
+ end
128
146
  end
129
147
 
130
148
  describe "MySQL Backups" do
data/spec/helpers.rb CHANGED
@@ -18,19 +18,29 @@ module Helpers
18
18
  FakeWeb.register_uri(:post, "http://example.org/api/environments", :body => JSON.generate({
19
19
  @mock_environment_name => {:id => 1}
20
20
  }))
21
-
22
- FileUtils.mkdir_p(tmp_dir)
23
- Dir.glob("#{tmp_dir}/*").each do |f|
21
+
22
+ create_or_clean_dir(tmp_dir)
23
+ create_or_clean_dir(log_dir)
24
+
25
+ # FileUtils.mkdir_p(tmp_dir)
26
+ # FileUtils.mkdir_p(log_dir)
27
+ # Dir.glob("#{tmp_dir}/*").each do |f|
28
+ # FileUtils.rm(f)
29
+ # end
30
+ stub_configs
31
+ end
32
+
33
+ def create_or_clean_dir(dir)
34
+ FileUtils.mkdir_p(dir)
35
+ Dir.glob("#{dir}/*").each do |f|
24
36
  FileUtils.rm(f)
25
37
  end
26
-
27
- stub_configs
28
38
  end
29
39
 
30
40
  def stub_configs
31
41
  #print "in stub_configs: #{YAML::dump(@database_config)}, #{YAML::dump(spec_config)}\n"
32
42
  config = @database_config || spec_config
33
- config = config.merge({ :tmp_dir => tmp_dir })
43
+ config = config.merge({ :tmp_dir => tmp_dir, :log_dir => log_dir })
34
44
  # print "in stub_configs2: #{YAML::dump(config)}\n"
35
45
  File.open(backup_config_file, "w") do |f|
36
46
  f.puts YAML.dump(config )
@@ -55,7 +65,7 @@ module Helpers
55
65
  return File.read(PUBLIC_KEY_PATH), File.read(PRIVATE_KEY_PATH)
56
66
  end
57
67
 
58
- def run_after
68
+ def run_after(skip_file_remove = false)
59
69
  FileUtils.rm_f(backup_config_file)
60
70
  filenames = Dir.glob("#{tmp_dir}/*")
61
71
  if filenames.any?
@@ -183,6 +193,14 @@ module Helpers
183
193
  spec_config['tmp_dir'] || File.dirname("./tmp/")
184
194
  end
185
195
  end
196
+
197
+ def log_dir
198
+ if spec_config['log_dir'].nil?
199
+ File.dirname(__FILE__) + "/log/"
200
+ else
201
+ spec_config['log_dir'] || File.dirname("./log/")
202
+ end
203
+ end
186
204
 
187
205
  def mysql_user
188
206
  spec_config['mysql_user'] || "root"
@@ -280,6 +298,10 @@ module Helpers
280
298
  def stdout
281
299
  EY::Backup.logger.stdout.string
282
300
  end
301
+
302
+ def stderr
303
+ EY::Backup.logger.stderr.string
304
+ end
283
305
 
284
306
  load_spec_config
285
307
  end
@@ -0,0 +1 @@
1
+ 2017-05-17 02:10:11 -0400 1.7 KB
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ey_cloud_server
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.58
4
+ version: 1.4.60
5
5
  platform: ruby
6
6
  authors:
7
7
  - EngineYard
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-02-04 00:00:00.000000000 Z
11
+ date: 2017-05-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: json
@@ -78,34 +78,20 @@ dependencies:
78
78
  - - <
79
79
  - !ruby/object:Gem::Version
80
80
  version: '3.0'
81
- - !ruby/object:Gem::Dependency
82
- name: aws-s3
83
- requirement: !ruby/object:Gem::Requirement
84
- requirements:
85
- - - ! '>='
86
- - !ruby/object:Gem::Version
87
- version: 0.6.3
88
- type: :runtime
89
- prerelease: false
90
- version_requirements: !ruby/object:Gem::Requirement
91
- requirements:
92
- - - ! '>='
93
- - !ruby/object:Gem::Version
94
- version: 0.6.3
95
81
  - !ruby/object:Gem::Dependency
96
82
  name: fog-aws
97
83
  requirement: !ruby/object:Gem::Requirement
98
84
  requirements:
99
85
  - - ! '>='
100
86
  - !ruby/object:Gem::Version
101
- version: 0.3.0
87
+ version: 1.2.1
102
88
  type: :runtime
103
89
  prerelease: false
104
90
  version_requirements: !ruby/object:Gem::Requirement
105
91
  requirements:
106
92
  - - ! '>='
107
93
  - !ruby/object:Gem::Version
108
- version: 0.3.0
94
+ version: 1.2.1
109
95
  - !ruby/object:Gem::Dependency
110
96
  name: ey_enzyme
111
97
  requirement: !ruby/object:Gem::Requirement
@@ -293,6 +279,7 @@ files:
293
279
  - spec/gpg.public
294
280
  - spec/gpg.sekrit
295
281
  - spec/helpers.rb
282
+ - spec/log/ey_flex_postgresql_db_wanting.sizes
296
283
  - spec/snapshot_minder_spec.rb
297
284
  - spec/spec_helper.rb
298
285
  homepage: http://developer.engineyard.com
@@ -333,6 +320,7 @@ test_files:
333
320
  - spec/gpg.public
334
321
  - spec/gpg.sekrit
335
322
  - spec/helpers.rb
323
+ - spec/log/ey_flex_postgresql_db_wanting.sizes
336
324
  - spec/snapshot_minder_spec.rb
337
325
  - spec/spec_helper.rb
338
326
  - features/downloading_a_mysql_backup.feature