mysql-slaver 0.1.8 → 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.md +5 -6
- data/lib/mysql_slaver/cli.rb +8 -3
- data/lib/mysql_slaver/db_copier.rb +23 -4
- data/lib/mysql_slaver/master_changer.rb +23 -3
- data/lib/mysql_slaver/mysql_command.rb +10 -10
- data/lib/mysql_slaver/slaver.rb +11 -4
- data/lib/mysql_slaver/status_fetcher.rb +7 -5
- data/spec/mysql_slaver/db_copier_spec.rb +30 -3
- data/spec/mysql_slaver/master_changer_spec.rb +22 -1
- data/spec/mysql_slaver/slaver_spec.rb +30 -0
- data/spec/mysql_slaver/status_fetcher_spec.rb +11 -1
- metadata +4 -4
data/README.md
CHANGED
@@ -14,15 +14,15 @@ ASSUMPTIONS/PRE-REQUISITES
|
|
14
14
|
|
15
15
|
* localhost is configured as a mysql replication slave
|
16
16
|
* the current localhost user can ssh to the db master
|
17
|
+
* any ssh config settings required to access the master from localhost are set in a ~/.ssh/config file
|
18
|
+
* ssh is on the current user's path
|
17
19
|
* your mysql administrator user is called 'root', locally and on the db master
|
18
|
-
* root user has the same password on this host and the master server
|
19
20
|
* mysql is on the local user's path
|
20
21
|
* mysql and mysqldump are on the remote ssh user's path
|
21
22
|
* replication permissions from the local host to the db master are already setup
|
22
|
-
*
|
23
|
-
*
|
23
|
+
* root user has the same password on this host and the master server
|
24
|
+
* mysql socket (if any) is the same on localhost and the db master
|
24
25
|
* db character set is UTF-8
|
25
|
-
* any ssh config settings for the host are set in a ~/.ssh/config file
|
26
26
|
|
27
27
|
CAVEATS
|
28
28
|
|
@@ -35,7 +35,6 @@ TODO
|
|
35
35
|
* check ssh connection and permissions
|
36
36
|
* check replication permissions
|
37
37
|
* check slave is setup as a replication slave (i.e. it has a mysql server id)
|
38
|
-
* allow
|
39
|
-
* allow overriding the mysql root user
|
38
|
+
* allow a mysql admin username other than 'root'
|
40
39
|
* allow different root user passwords on slave and master
|
41
40
|
* allow ssh options
|
data/lib/mysql_slaver/cli.rb
CHANGED
@@ -4,13 +4,18 @@ module MysqlSlaver
|
|
4
4
|
option :database, :required => true, :desc => "The database to copy from the master"
|
5
5
|
option :replication_user, :required => true, :desc => "DB user (on the master host), with replication permissions"
|
6
6
|
option :replication_password, :required => true, :desc => "DB password for the replication user"
|
7
|
-
|
7
|
+
|
8
|
+
option :root_password, :desc => "Password for the mysql root user (on both master and slave)"
|
9
|
+
option :port, :desc => "Mysql port (if not 3306)"
|
10
|
+
option :sock, :desc => "Mysql socket file (if any)"
|
11
|
+
|
8
12
|
desc "enslave", "start mysql replication to this host from a master"
|
9
|
-
|
10
|
-
LONGDESC
|
13
|
+
|
11
14
|
def enslave
|
12
15
|
MysqlSlaver::Slaver.new(
|
13
16
|
:master_host => options[:master_host],
|
17
|
+
:port => options[:port],
|
18
|
+
:socket_file => options[:sock],
|
14
19
|
:mysql_root_password => options[:root_password],
|
15
20
|
:database => options[:database],
|
16
21
|
:replication_user => options[:replication_user],
|
@@ -2,22 +2,41 @@ module MysqlSlaver
|
|
2
2
|
class DbCopier
|
3
3
|
include MysqlCommand
|
4
4
|
|
5
|
-
attr_accessor :master_host, :mysql_root_password, :database, :executor
|
5
|
+
attr_accessor :master_host, :mysql_root_password, :database, :executor, :port, :socket_file
|
6
6
|
|
7
7
|
def initialize(params)
|
8
8
|
@master_host = params.fetch(:master_host)
|
9
9
|
@mysql_root_password = params.fetch(:mysql_root_password, '')
|
10
10
|
@database = params.fetch(:database)
|
11
|
+
@port = params.fetch(:port, nil)
|
12
|
+
@socket_file = params.fetch(:socket_file, nil)
|
11
13
|
@executor = params.fetch(:executor) { Executor.new }
|
12
14
|
end
|
13
15
|
|
14
16
|
def copy!
|
15
|
-
executor.execute mysql_command("stop slave",
|
16
|
-
cmd = mysqldump
|
17
|
+
executor.execute mysql_command("stop slave", mysql_params)
|
18
|
+
cmd = mysqldump
|
17
19
|
dump_cmd = executor.ssh_command(cmd, master_host)
|
18
|
-
load_cmd = ['mysql', mysql_credentials('root',
|
20
|
+
load_cmd = ['mysql', mysql_credentials('root', mysql_params), database].join(' ')
|
19
21
|
command = [dump_cmd, load_cmd].join(' | ')
|
20
22
|
executor.execute command
|
21
23
|
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def mysql_params
|
28
|
+
{
|
29
|
+
:root_password => mysql_root_password,
|
30
|
+
:socket_file => socket_file
|
31
|
+
}
|
32
|
+
end
|
33
|
+
|
34
|
+
def mysqldump
|
35
|
+
creds = mysql_credentials('root', mysql_params)
|
36
|
+
rtn = %[mysqldump]
|
37
|
+
rtn << %[ -P #{port}] if port
|
38
|
+
rtn << %[ #{creds} -h #{master_host} --master-data --single-transaction --quick --skip-add-locks --skip-lock-tables --default-character-set=utf8 --compress #{database}]
|
39
|
+
rtn
|
40
|
+
end
|
22
41
|
end
|
23
42
|
end
|
@@ -2,10 +2,12 @@ module MysqlSlaver
|
|
2
2
|
class MasterChanger
|
3
3
|
include MysqlCommand
|
4
4
|
|
5
|
-
attr_accessor :master_host, :mysql_root_password, :replication_user, :replication_password, :executor
|
5
|
+
attr_accessor :master_host, :mysql_root_password, :replication_user, :replication_password, :executor, :port, :socket_file
|
6
6
|
|
7
7
|
def initialize(params)
|
8
8
|
@master_host = params.fetch(:master_host)
|
9
|
+
@port = params.fetch(:port)
|
10
|
+
@socket_file = params.fetch(:socket_file, nil)
|
9
11
|
@mysql_root_password = params.fetch(:mysql_root_password, '')
|
10
12
|
@replication_user = params.fetch(:replication_user)
|
11
13
|
@replication_password = params.fetch(:replication_password)
|
@@ -18,14 +20,32 @@ module MysqlSlaver
|
|
18
20
|
change_master(status),
|
19
21
|
'start slave'
|
20
22
|
]
|
21
|
-
cmd = mysql_command(cmds.join('; '),
|
23
|
+
cmd = mysql_command(cmds.join('; '), mysql_params)
|
22
24
|
executor.execute cmd
|
23
25
|
end
|
24
26
|
|
25
27
|
private
|
26
28
|
|
29
|
+
def mysql_params
|
30
|
+
{
|
31
|
+
:root_password => mysql_root_password,
|
32
|
+
:socket_file => socket_file
|
33
|
+
}
|
34
|
+
end
|
35
|
+
|
27
36
|
def change_master(status)
|
28
|
-
%[CHANGE MASTER TO
|
37
|
+
%[CHANGE MASTER TO #{cmd_values(status)}]
|
38
|
+
end
|
39
|
+
|
40
|
+
def cmd_values(status)
|
41
|
+
[
|
42
|
+
"MASTER_LOG_FILE='#{status[:file]}'",
|
43
|
+
"MASTER_LOG_POS=#{status[:position]}",
|
44
|
+
"MASTER_HOST='#{master_host}'",
|
45
|
+
"MASTER_PORT=#{port}",
|
46
|
+
"MASTER_USER='#{replication_user}'",
|
47
|
+
"MASTER_PASSWORD='#{replication_password}'"
|
48
|
+
].join(', ')
|
29
49
|
end
|
30
50
|
end
|
31
51
|
end
|
@@ -1,19 +1,19 @@
|
|
1
1
|
module MysqlSlaver
|
2
2
|
module MysqlCommand
|
3
|
-
def mysql_credentials(user,
|
4
|
-
|
5
|
-
|
3
|
+
def mysql_credentials(user, params)
|
4
|
+
password = params.fetch(:root_password, "")
|
5
|
+
socket_file = params.fetch(:socket_file, nil)
|
6
|
+
|
7
|
+
rtn = ""
|
8
|
+
rtn << "-S #{socket_file}" if socket_file
|
9
|
+
rtn << " -u #{user}"
|
10
|
+
rtn << " -p #{password}" unless password.to_s == ""
|
6
11
|
rtn
|
7
12
|
end
|
8
13
|
|
9
|
-
def mysql_command(cmd,
|
10
|
-
creds = mysql_credentials('root',
|
14
|
+
def mysql_command(cmd, params)
|
15
|
+
creds = mysql_credentials('root', params)
|
11
16
|
%[mysql #{creds} -e "#{cmd}"]
|
12
17
|
end
|
13
|
-
|
14
|
-
def mysqldump(host, database, password)
|
15
|
-
creds = mysql_credentials('root', password)
|
16
|
-
%[mysqldump -h #{host} #{creds} --master-data --single-transaction --quick --skip-add-locks --skip-lock-tables --default-character-set=utf8 --compress #{database}]
|
17
|
-
end
|
18
18
|
end
|
19
19
|
end
|
data/lib/mysql_slaver/slaver.rb
CHANGED
@@ -9,12 +9,15 @@ module MysqlSlaver
|
|
9
9
|
attr_reader :status_fetcher, :data_copier, :master_changer
|
10
10
|
|
11
11
|
def initialize(params)
|
12
|
-
mysql_root_password
|
12
|
+
mysql_root_password = params.fetch(:mysql_root_password, '')
|
13
|
+
port = params.fetch(:port, 3306)
|
14
|
+
socket_file = params.fetch(:socket_file, nil)
|
13
15
|
|
14
16
|
@status_fetcher = params.fetch(:status_fetcher) {
|
15
17
|
StatusFetcher.new(
|
16
18
|
:master_host => params.fetch(:master_host),
|
17
|
-
:mysql_root_password => mysql_root_password
|
19
|
+
:mysql_root_password => mysql_root_password,
|
20
|
+
:socket_file => socket_file
|
18
21
|
)
|
19
22
|
}
|
20
23
|
|
@@ -22,7 +25,9 @@ module MysqlSlaver
|
|
22
25
|
DbCopier.new(
|
23
26
|
:master_host => params.fetch(:master_host),
|
24
27
|
:mysql_root_password => mysql_root_password,
|
25
|
-
:database => params.fetch(:database)
|
28
|
+
:database => params.fetch(:database),
|
29
|
+
:port => port,
|
30
|
+
:socket_file => socket_file
|
26
31
|
)
|
27
32
|
}
|
28
33
|
|
@@ -31,7 +36,9 @@ module MysqlSlaver
|
|
31
36
|
:master_host => params.fetch(:master_host),
|
32
37
|
:mysql_root_password => mysql_root_password,
|
33
38
|
:replication_user => params.fetch(:replication_user),
|
34
|
-
:replication_password => params.fetch(:replication_password)
|
39
|
+
:replication_password => params.fetch(:replication_password),
|
40
|
+
:port => port,
|
41
|
+
:socket_file => socket_file
|
35
42
|
)
|
36
43
|
}
|
37
44
|
end
|
@@ -3,16 +3,18 @@ module MysqlSlaver
|
|
3
3
|
include Logger
|
4
4
|
include MysqlCommand
|
5
5
|
|
6
|
-
attr_accessor :master_host, :mysql_root_password, :executor
|
6
|
+
attr_accessor :master_host, :mysql_root_password, :executor, :socket_file
|
7
7
|
|
8
8
|
def initialize(params)
|
9
|
-
@master_host
|
10
|
-
@
|
11
|
-
@
|
9
|
+
@master_host = params.fetch(:master_host)
|
10
|
+
@socket_file = params.fetch(:socket_file, nil)
|
11
|
+
@mysql_root_password = params.fetch(:mysql_root_password, '')
|
12
|
+
@executor = params.fetch(:executor) { Executor.new }
|
12
13
|
end
|
13
14
|
|
14
15
|
def status
|
15
|
-
|
16
|
+
params = {:root_password => mysql_root_password, :socket_file => socket_file}
|
17
|
+
cmd = mysql_command("show master status\\G", params)
|
16
18
|
data = executor.execute executor.ssh_command(cmd, master_host)
|
17
19
|
rtn = parse data
|
18
20
|
log "MASTER STATUS - file: #{rtn[:file]}, position: #{rtn[:position]}"
|
@@ -17,23 +17,50 @@ module MysqlSlaver
|
|
17
17
|
describe "#copy!" do
|
18
18
|
it "stops slave" do
|
19
19
|
copier.copy!
|
20
|
-
stop = %[mysql
|
20
|
+
stop = %[mysql -u root -p supersekrit -e "stop slave"]
|
21
21
|
expect(executor).to have_received(:execute).with(stop)
|
22
22
|
end
|
23
23
|
|
24
24
|
it "loads data" do
|
25
|
-
dump_and_load = "dummy-ssh-command | mysql
|
25
|
+
dump_and_load = "dummy-ssh-command | mysql -u root -p supersekrit myappdb"
|
26
26
|
expect(executor).to receive(:execute).once.ordered.with(dump_and_load)
|
27
27
|
copier.copy!
|
28
28
|
end
|
29
29
|
|
30
30
|
context "dumping" do
|
31
31
|
it "issues mysqldump over ssh" do
|
32
|
-
dump = "mysqldump -h my.db.host
|
32
|
+
dump = "mysqldump -u root -p supersekrit -h my.db.host --master-data --single-transaction --quick --skip-add-locks --skip-lock-tables --default-character-set=utf8 --compress myappdb"
|
33
33
|
expect(executor).to receive(:ssh_command).with(dump, 'my.db.host')
|
34
34
|
copier.copy!
|
35
35
|
end
|
36
36
|
end
|
37
37
|
end
|
38
|
+
|
39
|
+
context "with a non-standard mysql port" do
|
40
|
+
let(:params) { super().merge(:port => 3307) }
|
41
|
+
|
42
|
+
it "issues mysqldump over ssh" do
|
43
|
+
dump = "mysqldump -P 3307 -u root -p supersekrit -h my.db.host --master-data --single-transaction --quick --skip-add-locks --skip-lock-tables --default-character-set=utf8 --compress myappdb"
|
44
|
+
expect(executor).to receive(:ssh_command).with(dump, 'my.db.host')
|
45
|
+
copier.copy!
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
context "with a socket file" do
|
50
|
+
let(:params) { super().merge(:socket_file => "/tmp/mysql.sock") }
|
51
|
+
|
52
|
+
it "issues mysqldump over ssh" do
|
53
|
+
dump = "mysqldump -S /tmp/mysql.sock -u root -p supersekrit -h my.db.host --master-data --single-transaction --quick --skip-add-locks --skip-lock-tables --default-character-set=utf8 --compress myappdb"
|
54
|
+
expect(executor).to receive(:ssh_command).with(dump, 'my.db.host')
|
55
|
+
copier.copy!
|
56
|
+
end
|
57
|
+
|
58
|
+
it "loads data" do
|
59
|
+
dump_and_load = "dummy-ssh-command | mysql -S /tmp/mysql.sock -u root -p supersekrit myappdb"
|
60
|
+
expect(executor).to receive(:execute).once.ordered.with(dump_and_load)
|
61
|
+
copier.copy!
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
38
65
|
end
|
39
66
|
end
|
@@ -8,6 +8,7 @@ module MysqlSlaver
|
|
8
8
|
let(:params) {
|
9
9
|
{
|
10
10
|
:master_host => 'my.db.host',
|
11
|
+
:port => 3306,
|
11
12
|
:mysql_root_password => 'supersekrit',
|
12
13
|
:replication_user => 'repluser',
|
13
14
|
:replication_password => 'replpassword',
|
@@ -18,7 +19,27 @@ module MysqlSlaver
|
|
18
19
|
|
19
20
|
describe "#change!" do
|
20
21
|
it "executes multi-part mysql command" do
|
21
|
-
change_cmd = %[mysql
|
22
|
+
change_cmd = %[mysql -u root -p supersekrit -e "stop slave; CHANGE MASTER TO MASTER_LOG_FILE='mysql-bin.001555', MASTER_LOG_POS=18426246, MASTER_HOST='my.db.host', MASTER_PORT=3306, MASTER_USER='repluser', MASTER_PASSWORD='replpassword'; start slave"]
|
23
|
+
changer.change!(status)
|
24
|
+
expect(executor).to have_received(:execute).with(change_cmd)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
context "with a non-standard mysql port" do
|
29
|
+
let(:params) { super().merge(:port => 3307) }
|
30
|
+
|
31
|
+
it "executes multi-part mysql command" do
|
32
|
+
change_cmd = %[mysql -u root -p supersekrit -e "stop slave; CHANGE MASTER TO MASTER_LOG_FILE='mysql-bin.001555', MASTER_LOG_POS=18426246, MASTER_HOST='my.db.host', MASTER_PORT=3307, MASTER_USER='repluser', MASTER_PASSWORD='replpassword'; start slave"]
|
33
|
+
changer.change!(status)
|
34
|
+
expect(executor).to have_received(:execute).with(change_cmd)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
context "with a mysql socket file" do
|
39
|
+
let(:params) { super().merge(:socket_file => "/tmp/mysql.sock") }
|
40
|
+
|
41
|
+
it "executes multi-part mysql command" do
|
42
|
+
change_cmd = %[mysql -S /tmp/mysql.sock -u root -p supersekrit -e "stop slave; CHANGE MASTER TO MASTER_LOG_FILE='mysql-bin.001555', MASTER_LOG_POS=18426246, MASTER_HOST='my.db.host', MASTER_PORT=3306, MASTER_USER='repluser', MASTER_PASSWORD='replpassword'; start slave"]
|
22
43
|
changer.change!(status)
|
23
44
|
expect(executor).to have_received(:execute).with(change_cmd)
|
24
45
|
end
|
@@ -64,6 +64,36 @@ module MysqlSlaver
|
|
64
64
|
expect(changer.mysql_root_password).to eq('supersekrit')
|
65
65
|
expect(changer.replication_user).to eq('repluser')
|
66
66
|
expect(changer.replication_password).to eq('replpassword')
|
67
|
+
expect(changer.port).to eq(3306)
|
68
|
+
end
|
69
|
+
|
70
|
+
context "with non-standard mysql port" do
|
71
|
+
let(:params) { super().merge(:port => 3307) }
|
72
|
+
|
73
|
+
it "instantiates a master changer" do
|
74
|
+
changer = slaver.master_changer
|
75
|
+
expect(changer.port).to eq(3307)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
context "with mysql socket" do
|
80
|
+
let(:params) { super().merge(:socket_file => "/tmp/mysql.sock") }
|
81
|
+
|
82
|
+
it "instantiates a status fetcher" do
|
83
|
+
fetcher = slaver.status_fetcher
|
84
|
+
expect(fetcher.socket_file).to eq("/tmp/mysql.sock")
|
85
|
+
end
|
86
|
+
|
87
|
+
it "instantiates a master changer" do
|
88
|
+
changer = slaver.master_changer
|
89
|
+
expect(changer.socket_file).to eq("/tmp/mysql.sock")
|
90
|
+
end
|
91
|
+
|
92
|
+
it "instantiates a data copier" do
|
93
|
+
copier = slaver.data_copier
|
94
|
+
expect(copier.socket_file).to eq("/tmp/mysql.sock")
|
95
|
+
end
|
96
|
+
|
67
97
|
end
|
68
98
|
end
|
69
99
|
end
|
@@ -32,7 +32,7 @@ EOF
|
|
32
32
|
end
|
33
33
|
|
34
34
|
it "executes show master command over ssh" do
|
35
|
-
show_master = %[mysql
|
35
|
+
show_master = %[mysql -u root -p supersekrit -e "show master status\\G"]
|
36
36
|
fetcher.status
|
37
37
|
expect(executor).to have_received(:ssh_command).with(show_master, 'my.db.host')
|
38
38
|
end
|
@@ -41,5 +41,15 @@ EOF
|
|
41
41
|
expect(fetcher.status).to eq({:file => 'mysql-bin.003219', :position => '37065270'})
|
42
42
|
end
|
43
43
|
end
|
44
|
+
|
45
|
+
context "using a socket filename" do
|
46
|
+
let(:params) { super().merge(:socket_file => "/var/run/mysqld/mysqld.master.sock") }
|
47
|
+
|
48
|
+
it "executes show master command over ssh" do
|
49
|
+
show_master = %[mysql -S /var/run/mysqld/mysqld.master.sock -u root -p supersekrit -e "show master status\\G"]
|
50
|
+
fetcher.status
|
51
|
+
expect(executor).to have_received(:ssh_command).with(show_master, 'my.db.host')
|
52
|
+
end
|
53
|
+
end
|
44
54
|
end
|
45
55
|
end
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: mysql-slaver
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 9
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
8
|
- 1
|
9
|
-
-
|
10
|
-
version: 0.1.
|
9
|
+
- 9
|
10
|
+
version: 0.1.9
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- David Salgado
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2014-04-
|
18
|
+
date: 2014-04-18 00:00:00 Z
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|
21
21
|
name: rspec
|