ey_cloud_server 1.4.47 → 1.4.49
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -7
- data/features/support/env.rb +3 -3
- data/lib/ey-flex.rb +2 -3
- data/lib/ey-flex/snapshot_minder.rb +1 -1
- data/lib/ey_backup.rb +8 -2
- data/lib/ey_backup/backend.rb +1 -1
- data/lib/ey_backup/base.rb +1 -1
- data/lib/ey_backup/cli.rb +47 -19
- data/lib/ey_backup/database.rb +15 -1
- data/lib/ey_backup/dumper.rb +31 -3
- data/lib/ey_backup/engine.rb +3 -3
- data/lib/ey_backup/engines/mysql_engine.rb +49 -7
- data/lib/ey_backup/engines/postgresql_engine.rb +69 -21
- data/lib/ey_backup/logger.rb +54 -4
- data/lib/ey_backup/spawner.rb +31 -5
- data/lib/ey_cloud_server.rb +0 -1
- data/lib/ey_cloud_server/version.rb +1 -1
- data/spec/config.yml +11 -0
- data/spec/ey_backup/mysql_backups_spec.rb +11 -10
- data/spec/ey_backup/postgres_backups_spec.rb +9 -9
- data/spec/helpers.rb +16 -10
- data/spec/spec_helper.rb +1 -2
- metadata +126 -141
- data/bin/binary_log_purge +0 -263
- data/bin/clear_binlogs_from_slave +0 -7
- data/bin/ey-agent +0 -5
- data/bin/mysql_start +0 -6
- data/lib/ey-flex/big-brother.rb +0 -80
- data/lib/ey_cloud_server/mysql_start.rb +0 -155
- data/spec/big-brother_spec.rb +0 -12
@@ -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} --format=c -Upostgres #{database_name}"
|
9
|
+
command = "PGPASSWORD='#{password}' pg_dump -h #{host} --create --format=c -Upostgres #{database_name} 2> /tmp/eybackup.$$.dumperr"
|
10
10
|
|
11
11
|
if gpg?
|
12
12
|
command << " | " << GPGEncryptor.command_for(key_id)
|
@@ -15,17 +15,13 @@ module EY
|
|
15
15
|
|
16
16
|
command << " > #{file}"
|
17
17
|
|
18
|
-
run(command)
|
18
|
+
run(command, database_name)
|
19
19
|
|
20
20
|
file
|
21
21
|
end
|
22
22
|
|
23
23
|
def load(database_name, file)
|
24
|
-
|
25
|
-
cycle_database(database_name)
|
26
|
-
else
|
27
|
-
create_database(database_name)
|
28
|
-
end
|
24
|
+
cycle_database(database_name)
|
29
25
|
|
30
26
|
command = "cat #{file}"
|
31
27
|
|
@@ -33,33 +29,85 @@ module EY
|
|
33
29
|
raise "Cannot load a GPG backup"
|
34
30
|
end
|
35
31
|
|
36
|
-
command << " | PGPASSWORD='#{password}' pg_restore -h #{host} --format=c -Upostgres -d #{database_name}"
|
37
|
-
|
38
|
-
run(command)
|
39
|
-
end
|
32
|
+
command << " | PGPASSWORD='#{password}' pg_restore -h #{host} --clean --format=c -Upostgres -d #{database_name}"
|
40
33
|
|
41
|
-
|
42
|
-
runs?("PGPASSWORD='#{password}' psql -l -h #{host} | grep '#{database_name}'")
|
34
|
+
run(command, database_name)
|
43
35
|
end
|
44
36
|
|
45
|
-
def
|
37
|
+
def check_connections(database_name)
|
46
38
|
stdout = StringIO.new()
|
47
|
-
active_connections = spawn(%Q{PGPASSWORD='#{password}' psql -U postgres -t -c "select count(*) from pg_stat_activity where datname='#{database_name}';"}, stdout)
|
39
|
+
active_connections = spawn(%Q{PGPASSWORD='#{password}' psql -U postgres -h #{host} -t -c "select count(*) from pg_stat_activity where datname='#{database_name}';"}, stdout)
|
48
40
|
|
49
41
|
if stdout.string.to_i > 0
|
50
|
-
|
42
|
+
res = ''
|
43
|
+
unless force
|
44
|
+
puts "There are currently #{stdout.string.to_i} connections on database: '#{database_name}'; can I kill these to continue (Y/n):"
|
45
|
+
Timeout::timeout(30){
|
46
|
+
res = gets.strip
|
47
|
+
}
|
48
|
+
end
|
49
|
+
|
50
|
+
if res.upcase == 'Y' or force
|
51
|
+
cancel_connections(database_name)
|
52
|
+
else
|
53
|
+
EY::Backup.logger.fatal(%Q{ERROR: Target database has active connections. For more information, see "Restore or load a database" in docs.engineyard.com})
|
54
|
+
end
|
51
55
|
end
|
52
|
-
|
53
|
-
|
56
|
+
end
|
57
|
+
|
58
|
+
def cancel_connections(database_name)
|
59
|
+
spawn(%Q{psql -U postgres -h #{host} -c"SELECT pg_terminate_backend(pg_stat_activity.pid)
|
60
|
+
FROM pg_stat_activity
|
61
|
+
WHERE pg_stat_activity.datname = '#{database_name}';"})
|
62
|
+
end
|
63
|
+
|
64
|
+
def drop_database(database_name)
|
65
|
+
spawn("PGPASSWORD='#{password}' dropdb -h #{host} -Upostgres #{database_name}")
|
54
66
|
end
|
55
67
|
|
56
68
|
def create_database(database_name)
|
57
69
|
spawn("PGPASSWORD='#{password}' createdb -U#{username} -h #{host} #{database_name}")
|
58
70
|
end
|
59
|
-
|
71
|
+
|
72
|
+
def check_if_replica
|
73
|
+
stdout = StringIO.new()
|
74
|
+
spawn(%Q{PGPASSWORD='#{password}' psql -U postgres -h #{host} -t -c "select pg_is_in_recovery()"| head -n 1}, stdout)
|
75
|
+
unless stdout.string.chomp =~ /^\W*f$/
|
76
|
+
EY::Backup.logger.fatal(%Q{ERROR: Target host: '#{host}' is currently a replica in recovery mode; restore operations need to be processed against the master.})
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def create_command(database_name)
|
81
|
+
stdout = StringIO.new()
|
82
|
+
|
83
|
+
spawn(%Q{PGPASSWORD='#{password}' psql -U postgres -h #{host} -t -c "SELECT 'CREATE DATABASE ' || datname ||
|
84
|
+
' WITH OWNER ' || pg_user.usename ||
|
85
|
+
CASE (select pg_encoding_to_char(encoding) from pg_database where datname='template1')
|
86
|
+
WHEN pg_encoding_to_char(encoding)
|
87
|
+
THEN ''
|
88
|
+
ELSE ' template=template0'
|
89
|
+
END ||
|
90
|
+
' ENCODING ''' || pg_encoding_to_char(encoding) ||
|
91
|
+
''' LC_COLLATE ''' || datcollate||
|
92
|
+
''' LC_CTYPE ''' || datctype ||
|
93
|
+
''' CONNECTION LIMIT ' || datconnlimit || ';'
|
94
|
+
FROM pg_database
|
95
|
+
INNER JOIN pg_user
|
96
|
+
ON pg_user.usesysid = pg_database.datdba
|
97
|
+
WHERE datname = '#{database_name}'"
|
98
|
+
}, stdout)
|
99
|
+
stdout.string
|
100
|
+
end
|
101
|
+
|
60
102
|
def cycle_database(database_name)
|
61
|
-
|
62
|
-
|
103
|
+
create_cmd = create_command(database_name).chomp
|
104
|
+
if create_cmd == ''
|
105
|
+
create_database(database_name)
|
106
|
+
else
|
107
|
+
check_connections(database_name)
|
108
|
+
drop_database(database_name)
|
109
|
+
spawn(%Q{PGPASSWORD='#{password}' psql -U postgres -h #{host} -t -c "#{create_cmd}"})
|
110
|
+
end
|
63
111
|
end
|
64
112
|
|
65
113
|
def suffix
|
data/lib/ey_backup/logger.rb
CHANGED
@@ -2,15 +2,13 @@ module EY
|
|
2
2
|
module Backup
|
3
3
|
class Logger
|
4
4
|
extend Forwardable
|
5
|
+
require 'shellwords'
|
5
6
|
|
6
7
|
attr_accessor :stdout, :stderr
|
7
8
|
|
8
9
|
alias_method :fatal, :abort
|
9
10
|
public :fatal
|
10
11
|
|
11
|
-
def_delegator :stderr, :puts, :error
|
12
|
-
def_delegator :stderr, :puts, :warn
|
13
|
-
def_delegator :stdout, :puts, :info
|
14
12
|
def_delegator :stdout, :puts, :puts
|
15
13
|
|
16
14
|
def self.quiet
|
@@ -19,18 +17,70 @@ module EY
|
|
19
17
|
|
20
18
|
def initialize(stdout = $stdout, stderr = $stderr)
|
21
19
|
@stdout, @stderr = stdout, stderr
|
20
|
+
@verbose = false
|
21
|
+
end
|
22
|
+
|
23
|
+
def push_dashboard_alert(message, alert_level, db = nil)
|
24
|
+
message.gsub!("\n", '\n')
|
25
|
+
type="process-dbbackup"
|
26
|
+
type = type + " #{db}" unless db.nil?
|
27
|
+
full_txt= "Severity: #{alert_level}\n" \
|
28
|
+
+ "Time: #{Time.now.to_i}\n" \
|
29
|
+
+ "Type: #{type}\n" \
|
30
|
+
+ "Plugin: exec\n" \
|
31
|
+
+ "raw_message: '#{message}'"
|
32
|
+
full_txt = Shellwords.escape(full_txt)
|
33
|
+
alert_command = %Q(echo #{full_txt} | /engineyard/bin/ey-alert.rb 2>&1)
|
34
|
+
verbose "Sending dashboard alert"
|
35
|
+
system(alert_command)
|
36
|
+
end
|
37
|
+
|
38
|
+
def info(msg)
|
39
|
+
puts "#{Time.now} #{msg}"
|
40
|
+
end
|
41
|
+
|
42
|
+
def verbose(msg)
|
43
|
+
info(msg) if @verbose
|
44
|
+
end
|
45
|
+
|
46
|
+
def set_verbose()
|
47
|
+
@verbose = true
|
22
48
|
end
|
23
49
|
|
24
50
|
def say(msg, newline = true)
|
25
51
|
newline ? info(msg) : stdout.print(msg)
|
26
52
|
end
|
53
|
+
|
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.
|
57
|
+
end
|
58
|
+
|
59
|
+
def warn(msg, db = nil)
|
60
|
+
stderr.puts("#{Time.now} WARNING: #{msg}")
|
61
|
+
push_dashboard_alert(msg, "WARNING", db)
|
62
|
+
end
|
63
|
+
|
64
|
+
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)
|
67
|
+
end
|
68
|
+
|
69
|
+
def exception(type, msg)
|
70
|
+
self.error(msg)
|
71
|
+
end
|
72
|
+
|
73
|
+
def debug(msg)
|
74
|
+
stderr.puts("#{Time.now} DEBUG: #{msg}")
|
75
|
+
end
|
76
|
+
|
27
77
|
end
|
28
78
|
|
29
79
|
module Logging
|
30
80
|
extend Forwardable
|
31
81
|
|
32
82
|
def_delegator EY::Backup, :logger
|
33
|
-
def_delegators :logger, :fatal, :error, :warn, :info, :puts, :say
|
83
|
+
def_delegators :logger, :fatal, :error, :warn, :okay, :info, :verbose, :debug, :puts, :say
|
34
84
|
end
|
35
85
|
end
|
36
86
|
end
|
data/lib/ey_backup/spawner.rb
CHANGED
@@ -18,15 +18,41 @@ module EY
|
|
18
18
|
end
|
19
19
|
end
|
20
20
|
|
21
|
-
def run(command)
|
22
|
-
unless runs?(command)
|
23
|
-
raise "Failed to run
|
21
|
+
def run(command, db = nil)
|
22
|
+
unless runs?(command, db)
|
23
|
+
raise "Failed to run backup command."
|
24
24
|
end
|
25
25
|
end
|
26
26
|
|
27
|
-
def runs?(command)
|
28
|
-
|
27
|
+
def runs?(command, db)
|
28
|
+
# This is to detect failures anywhere in the pipeline.
|
29
|
+
wrapper = <<-EOT
|
30
|
+
status=0
|
31
|
+
set -o pipefail
|
32
|
+
#{command}
|
33
|
+
status=$?
|
34
|
+
|
35
|
+
if [ $status -gt 0 ]; then exit 1; fi
|
36
|
+
EOT
|
37
|
+
|
38
|
+
verbose "Running command: #{command}"
|
39
|
+
pid, *_ = Open4.popen4(wrapper)
|
29
40
|
pid, status = Process::waitpid2(pid)
|
41
|
+
|
42
|
+
if ! status.success?
|
43
|
+
dumperr = File.read("/tmp/eybackup.#{pid}.dumperr")
|
44
|
+
error("DB dump failed. The error returned was: #{dumperr}", db)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Clean up:
|
48
|
+
# This whole error output thing is hideous.
|
49
|
+
# We need the errors, but as we're dealing with a pipeline we
|
50
|
+
# need to capture them separately rather than send them as
|
51
|
+
# input to the next process. We construct the backup command
|
52
|
+
# in the backup engine but only know the pid of the resulting
|
53
|
+
# backup here. Ugh.
|
54
|
+
system("rm /tmp/eybackup.#{pid}.dumperr") if File.exists?("/tmp/eybackup.#{pid}.dumperr")
|
55
|
+
|
30
56
|
status.success?
|
31
57
|
end
|
32
58
|
|
data/lib/ey_cloud_server.rb
CHANGED
data/spec/config.yml
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
---
|
2
|
+
tmp_dir:
|
3
|
+
data_json:
|
4
|
+
mysql_user: root
|
5
|
+
mysql_password:
|
6
|
+
mysql_host: localhost
|
7
|
+
postgresql_user: postgres
|
8
|
+
postgresql_password: ketchup
|
9
|
+
postgresql_host: localhost
|
10
|
+
aws_secret_id: 'AKIAIDNXVLT3THIWYMNQ'
|
11
|
+
aws_secret_key: 'DZBOcAI1qpYF0V+ZL2SeA7Jzt15boFxIo+BD7vda'
|
@@ -23,6 +23,7 @@ describe "MySQL Backups" do
|
|
23
23
|
end
|
24
24
|
|
25
25
|
it "makes GPG encrypted backups" do
|
26
|
+
import_gpg
|
26
27
|
EY::Backup.run(["-c", backup_config_file, '-k', Helpers::PUBLIC_KEY_ID])
|
27
28
|
|
28
29
|
EY::Backup.run(["-c", backup_config_file, "-l", @db_name])
|
@@ -89,29 +90,29 @@ describe "MySQL Backups" do
|
|
89
90
|
it 'manually restores an encrypted backup' do
|
90
91
|
run_sql("CREATE TABLE `foo` (`id` int(11) NOT NULL auto_increment, PRIMARY KEY(`id`));", @db_name).should be_true
|
91
92
|
run_sql("CREATE TABLE `bar` (`id` int(11) NOT NULL auto_increment, PRIMARY KEY(`id`));", @db_name).should be_true
|
92
|
-
|
93
|
+
|
93
94
|
import_gpg
|
94
|
-
|
95
|
+
|
95
96
|
EY::Backup.run(["-c", backup_config_file, '-k', Helpers::PUBLIC_KEY_ID])
|
96
|
-
|
97
|
+
|
97
98
|
run_sql("DROP TABLE `bar`;", @db_name).should be_true
|
98
99
|
run_sql("SELECT * FROM `bar`;", @db_name).should be_false
|
99
|
-
|
100
|
+
|
100
101
|
reset_logger
|
101
|
-
|
102
|
+
|
102
103
|
EY::Backup.run(["-c", backup_config_file, "-l", @db_name])
|
103
|
-
|
104
|
+
|
104
105
|
stdout.should include("1 backup(s) found")
|
105
|
-
|
106
|
+
|
106
107
|
EY::Backup.run(["-c", backup_config_file, "--download", "0:#{@db_name}"])
|
107
108
|
file = Dir["#{EY::Backup.tmp_dir}/*#{@db_name}*"].first
|
108
109
|
stdout = StringIO.new
|
109
110
|
EY::Backup::Spawner.spawn("gpg --no-default-keyring --keyring #{Helpers::PUBLIC_KEY_PATH} --secret-keyring #{Helpers::PRIVATE_KEY_PATH} --decrypt", stdout, file)
|
110
|
-
|
111
|
+
|
111
112
|
stdout.rewind
|
112
|
-
|
113
|
+
|
113
114
|
run_sql_pipe(stdout, @db_name)
|
114
|
-
|
115
|
+
|
115
116
|
run_sql("SELECT * FROM `foo`;", @db_name).should be_true
|
116
117
|
run_sql("SELECT * FROM `bar`;", @db_name).should be_true
|
117
118
|
FileUtils.rm(file)
|
@@ -24,11 +24,11 @@ describe "Postgres Backups" do
|
|
24
24
|
|
25
25
|
it "makes GPG encrypted backups" do
|
26
26
|
import_gpg
|
27
|
-
|
27
|
+
|
28
28
|
EY::Backup.run(["-c", backup_config_file, '-e', 'postgresql', '-k', Helpers::PUBLIC_KEY_ID])
|
29
|
-
|
29
|
+
|
30
30
|
EY::Backup.run(["-c", backup_config_file, '-e', 'postgresql', "-l", @db_name])
|
31
|
-
|
31
|
+
|
32
32
|
stdout.should match(/0:#{@db_name}/)
|
33
33
|
end
|
34
34
|
|
@@ -56,19 +56,19 @@ describe "Postgres Backups" do
|
|
56
56
|
it "restores split backups" do
|
57
57
|
run_psql("CREATE TABLE foo (id integer NOT NULL);", @db_name).should be_true
|
58
58
|
run_psql("CREATE TABLE bar (id integer NOT NULL);", @db_name).should be_true
|
59
|
-
|
59
|
+
|
60
60
|
EY::Backup.run(["-c", backup_config_file, "-e", "postgresql", "-s", "100"])
|
61
61
|
EY::Backup.run(["-c", backup_config_file, "-e", "postgresql", "-l", @db_name]).first.parts.should > 1
|
62
|
-
|
62
|
+
|
63
63
|
run_psql("DROP TABLE bar;", @db_name).should be_true
|
64
64
|
run_psql("SELECT * FROM bar;", @db_name).should be_false
|
65
|
-
|
65
|
+
|
66
66
|
EY::Backup.run(["-c", backup_config_file, "-l", @db_name, '-e', 'postgresql'])
|
67
|
-
|
67
|
+
|
68
68
|
stdout.should include("1 backup(s) found")
|
69
|
-
|
69
|
+
|
70
70
|
EY::Backup.run(["-c", backup_config_file, "-r", "0:#{@db_name}", '-e', 'postgresql'])
|
71
|
-
|
71
|
+
|
72
72
|
run_psql("SELECT * FROM foo;", @db_name).should be_true
|
73
73
|
run_psql("SELECT * FROM bar;", @db_name).should be_true
|
74
74
|
end
|
data/spec/helpers.rb
CHANGED
@@ -4,7 +4,7 @@ module Helpers
|
|
4
4
|
PUBLIC_KEY_ID = '4D5AA1CE74A97ADB'
|
5
5
|
|
6
6
|
def run_before
|
7
|
-
|
7
|
+
read_keys
|
8
8
|
|
9
9
|
FakeWeb.clean_registry
|
10
10
|
FakeWeb.passthrough_uri(:any, %r{^https?://s3.amazonaws.com/})
|
@@ -28,7 +28,7 @@ module Helpers
|
|
28
28
|
end
|
29
29
|
|
30
30
|
def stub_configs
|
31
|
-
#
|
31
|
+
#print "in stub_configs: #{YAML::dump(@database_config)}, #{YAML::dump(spec_config)}\n"
|
32
32
|
config = @database_config || spec_config
|
33
33
|
config = config.merge({ :tmp_dir => tmp_dir })
|
34
34
|
# print "in stub_configs2: #{YAML::dump(config)}\n"
|
@@ -59,7 +59,7 @@ module Helpers
|
|
59
59
|
FileUtils.rm_f(backup_config_file)
|
60
60
|
filenames = Dir.glob("#{tmp_dir}/*")
|
61
61
|
if filenames.any?
|
62
|
-
raise "failed to remove #{filenames.join(' ')}"
|
62
|
+
raise "failed to remove #{filenames.join(', ')}"
|
63
63
|
end
|
64
64
|
end
|
65
65
|
|
@@ -82,7 +82,7 @@ module Helpers
|
|
82
82
|
end
|
83
83
|
|
84
84
|
def mysql_password_option
|
85
|
-
mysql_password.
|
85
|
+
mysql_password.nil? ? "" : "-p#{mysql_password}"
|
86
86
|
end
|
87
87
|
|
88
88
|
def generate_database_name(type)
|
@@ -156,7 +156,7 @@ module Helpers
|
|
156
156
|
stdin.flush
|
157
157
|
stdin.close
|
158
158
|
|
159
|
-
|
159
|
+
_, status = Process::waitpid2 pid
|
160
160
|
|
161
161
|
status
|
162
162
|
end
|
@@ -166,7 +166,7 @@ module Helpers
|
|
166
166
|
end
|
167
167
|
|
168
168
|
def tmp_dir
|
169
|
-
if spec_config['tmp_dir'].
|
169
|
+
if spec_config['tmp_dir'].nil?
|
170
170
|
File.dirname(__FILE__) + "/tmp/"
|
171
171
|
else
|
172
172
|
spec_config['tmp_dir'] || File.dirname("./tmp/")
|
@@ -174,11 +174,11 @@ module Helpers
|
|
174
174
|
end
|
175
175
|
|
176
176
|
def mysql_user
|
177
|
-
spec_config['mysql_user'] ||
|
177
|
+
spec_config['mysql_user'] || "root"
|
178
178
|
end
|
179
179
|
|
180
180
|
def postgresql_user
|
181
|
-
spec_config['postgresql_user'] ||
|
181
|
+
spec_config['postgresql_user'] || "postgres"
|
182
182
|
end
|
183
183
|
|
184
184
|
def mysql_password
|
@@ -190,11 +190,11 @@ module Helpers
|
|
190
190
|
end
|
191
191
|
|
192
192
|
def mysql_host
|
193
|
-
spec_config['mysql_host'] || '
|
193
|
+
spec_config['mysql_host'] || '127.0.0.1'
|
194
194
|
end
|
195
195
|
|
196
196
|
def postgresql_host
|
197
|
-
spec_config['postgresql_host'] || '
|
197
|
+
spec_config['postgresql_host'] || '127.0.0.1'
|
198
198
|
end
|
199
199
|
|
200
200
|
def created_mysql_dbs
|
@@ -215,8 +215,14 @@ module Helpers
|
|
215
215
|
|
216
216
|
def self.load_spec_config
|
217
217
|
filename = File.dirname(__FILE__) + '/config.yml'
|
218
|
+
|
218
219
|
if File.exist?(filename)
|
219
220
|
@spec_config = YAML.load_file(filename)
|
221
|
+
elsif Fog.mocking?
|
222
|
+
@spec_config = {
|
223
|
+
'aws_secret_id' => SecureRandom.base64(10),
|
224
|
+
'aws_secret_key' => SecureRandom.base64(20),
|
225
|
+
}
|
220
226
|
else
|
221
227
|
abort "Please setup your spec/config.yml file"
|
222
228
|
end
|