ey_cloud_server 1.4.58 → 1.4.60

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