repctl 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -25,6 +25,9 @@ MySQL from source, then do `make install`, then you are done! No MySQL
25
25
  post-installation steps are necessary. All post-install configuration is
26
26
  handled by `repctl`.
27
27
 
28
+ You will need to set an environment variable `REPCTL_CONFIG_DIR` in your shell
29
+ so that `repctl` can find the configuration files it needs.
30
+
28
31
  The top
29
32
 
30
33
  == Available Commands
data/bin/repctl CHANGED
@@ -1,195 +1,189 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  require 'socket'
4
+ require 'fileutils'
4
5
  require 'repctl'
5
6
  require 'thor'
6
7
 
7
- # Thor has the 'helpful' property that any Thor task gets executed only
8
- # once. The 'Helpers' module makes many Thor tasks available as ordinary
9
- # Ruby functions for internal use. The corresponding Thor tasks usually
10
- # delegate to the corresponding helper function with 'super'.
11
- module Helpers
12
-
13
- include Repctl::Config
14
- include Repctl::Servers
15
-
16
- def start(instance)
17
- say "Starting instance #{instance}.", :green
18
- do_start(instance)
19
- end
20
-
21
- def start_all
22
- all_instances.each do |instance|
23
- start(instance)
24
- end
25
- end
26
-
27
- def stop(instance)
28
- say "Stopping instance #{instance}.", :green
29
- do_admin(instance, "shutdown")
30
- end
31
-
32
- def stop_all
33
- all_live_instances.each do |instance|
34
- stop(instance)
8
+ module Repctl
9
+ module Helpers
10
+ def do_stop(instance)
11
+ do_admin(instance, "shutdown")
35
12
  end
36
- end
37
-
38
- def config(instance)
39
- say "Initializing new data directory for instance #{instance}."
40
- do_config(instance)
41
- end
42
-
43
- def config_all
44
- all_instances.each do |instance|
45
- config(instance)
46
- end
47
- end
48
-
49
- def secure_accounts(instance)
50
- do_secure_accounts(instance)
51
- end
52
13
 
53
- def secure_accounts_all
54
- Repctl::Servers.all_live_instances.each do |instance|
55
- secure_accounts(instance)
56
- end
57
- end
58
-
59
- def reset(instance)
60
- stop(instance)
61
- config(instance)
62
- start(instance)
63
- secure_accounts(instance)
64
- end
65
-
66
- def restart(instance)
67
- stop(instance)
68
- start(instance)
69
- end
70
-
71
- def reset_all
72
- all_instances.each do |instance|
73
- reset(instance)
74
- end
75
- end
76
-
77
- def change_master(master, slave, file, position)
78
- say "Changing master: master = #{master}, slave = #{slave}, file = #{file}, position = #{position}"
79
- do_change_master(master, slave, :file => file, :position => position)
80
- end
81
-
82
- def start_slave(slave)
83
- say "Starting slave #{slave}", :green
84
- run_mysql_query(slave, "START SLAVE")
85
- end
86
-
87
- def crash(instance)
88
- say "Crashing instance #{instance}", :red
89
- do_crash(instance)
90
- end
14
+ def do_reset(instance)
15
+ do_stop(instance)
16
+ do_config(instance)
17
+ do_start(instance)
18
+ do_secure_accounts(instance)
19
+ end
91
20
 
92
- def repl_user(instance)
93
- say "Creating replication account on instance #{instance}.", :green
94
- do_repl_user(instance)
95
- end
21
+ def do_start_slave(instance)
22
+ run_mysql_query(instance, "START SLAVE")
23
+ end
96
24
 
97
- def cluster_user(instance = 1)
98
- say "Installing cluster user for instance #{instance}.", :green
99
- do_cluster_user(instance)
25
+ def do_restart(instance)
26
+ do_admin(instance, "shutown")
27
+ do_start(instance)
28
+ end
100
29
  end
101
-
102
- end # Module helpers
30
+ end
103
31
 
104
- class Mysql < Thor
32
+ class RepctlCmds < Thor
105
33
 
106
34
  include Thor::Actions
107
35
  include Repctl::Config
108
36
  include Repctl::Commands
109
37
  include Repctl::Servers
110
- include Helpers
111
-
112
- desc "start INSTANCE", "Ensure that the given MySQL server instance is running."
113
- def start(instance)
114
- super
115
- end
116
-
117
- desc "start_all", "Start all the MySQL instances."
118
- def start_all
119
- super
120
- end
121
-
122
- desc "stop INSTANCE", "Stop a running MySQL server instance."
123
- def stop(instance)
124
- super
38
+ include Repctl::Helpers
39
+
40
+ desc "start [INSTANCES]", "Start one or more defined server instances."
41
+ method_option :all, :type => :boolean, :aliases => "-a",
42
+ :default => false, :desc => "Start all defined instances"
43
+ long_desc <<-EOS
44
+ Start one or more defined server instances. With the --all option,
45
+ all defined servers are started. Otherwise, you can give a list of
46
+ servers to start. If a server instance is already running, this
47
+ command has no effect on the instance.
48
+ EOS
49
+ def start(*instances)
50
+ if options[:all]
51
+ todos = all_instances
52
+ else
53
+ todos = instances
54
+ end
55
+ todos.each do |instance|
56
+ say "Starting instance #{instance}.", :green
57
+ do_start(instance)
58
+ end
125
59
  end
126
60
 
127
- desc "stop_all", "Stop all the MySQL servers."
128
- def stop_all
129
- super
61
+ desc "stop [INSTANCES]", "Stop one or more running server instances."
62
+ method_option :all, :type => :boolean, :aliases => "-a",
63
+ :default => false, :desc => "Stop all running instances"
64
+ long_desc <<-EOS
65
+ Stop one or more running server instances. With the --all option,
66
+ all running servers are stopped. Otherwise, you can give a list of
67
+ servers to stop.
68
+ EOS
69
+ def stop(*instances)
70
+ if options[:all]
71
+ todos = all_live_instances
72
+ else
73
+ todos = instances
74
+ end
75
+ todos.each do |instance|
76
+ say "Stopping instance #{instance}.", :green
77
+ do_stop(instance)
78
+ end
130
79
  end
131
80
 
132
- desc "config INSTANCE", "Initialize the data directory for a new instance."
133
- def config(instance)
134
- super
81
+ desc "config [INSTANCES]", "Initialize the data directory for server instances."
82
+ method_option :all, :type => :boolean, :aliases => "-a",
83
+ :default => false, :desc => "Initialize all defined instances"
84
+ long_desc <<-EOS
85
+ Initialize the data directory for one or more server instances.
86
+ With the --all option, the data directory for all defined servers
87
+ is initialized. Otherwise, you can give a list of defined server
88
+ instances.
89
+ EOS
90
+ def config(*instances)
91
+ if options[:all]
92
+ todos = all_instances
93
+ else
94
+ todos = instances
95
+ end
96
+ todos.each do |instance|
97
+ say "Initializing new data directory for instance #{instance}.", :green
98
+ do_config(instance)
99
+ end
135
100
  end
136
-
137
- desc "config_all", "Initialize the data directories for all instances."
138
- def config_all
139
- super
140
- end
141
-
142
- desc "secure_accounts INSTANCE", "Add passwords for root and anonymous accounts."
143
- def secure_accounts(instance)
144
- super
101
+
102
+ desc "secure_accounts [INSTANCES]", "Add password to root and anonymous accounts."
103
+ method_option :all, :type => :boolean, :aliases => "-a",
104
+ :default => false, :desc => "Secure accounts for all running instances"
105
+ long_desc <<-EOS
106
+ Add a password to the existing root and anonymous accounts of one or more
107
+ running server instances. With the --all option, all running servers
108
+ secured. Otherwise, you can give a list of servers to process.
109
+ EOS
110
+ def secure_accounts(*instances)
111
+ if options[:all]
112
+ todos = all_live_instances
113
+ else
114
+ todos = instances
115
+ end
116
+ todos.each do |instance|
117
+ say "Securing accounts for instance #{instance}.", :green
118
+ do_secure_accounts(instance)
119
+ end
145
120
  end
146
121
 
147
- desc "secure_accounts_all", "Add passwords for root and anonymous accounts for all instances."
148
- def secure_accounts_all
149
- super
122
+ desc "reset [INSTANCES]", "Reset one or more define server instances."
123
+ method_option :all, :type => :boolean, :aliases => "-a",
124
+ :default => false, :desc => "Reset all defined instances"
125
+ long_desc <<-EOS
126
+ Reset one or more defined server instances. If the --all option is
127
+ given the target servers are all defined servers, otherwise the
128
+ target servers are those specified in the instance argument.
129
+ If a target server instance is running, stop it. Then configure its
130
+ data directory, (re)start the server, and secure the accounts.
131
+ EOS
132
+ def reset(*instances)
133
+ if options[:all]
134
+ todos = all_instances
135
+ else
136
+ todos = instances
137
+ end
138
+ todos.each do |instance|
139
+ say "Resetting instance #{instance}.", :green
140
+ do_reset(instance)
141
+ end
150
142
  end
151
143
 
152
- desc "reset", "Remove the database data directory, reconfigure and restart the instance."
153
- def reset(instance)
154
- super
144
+ desc "restart INSTANCE" , "Stop and start a server instance."
145
+ def restart(instance)
146
+ say "Restarting instance #{instance}", :green
147
+ do_stop(instance)
148
+ do_start(instance)
155
149
  end
156
150
 
157
- desc "reset_all", "Remove all databases and restart MySQL instances."
158
- def reset_all
159
- super
160
- end
161
-
162
151
  desc "start_slave SLAVE", "Issue START SLAVE on the SLAVE MySQL instance."
163
152
  def start_slave(slave)
164
- super
153
+ say "Starting slave #{slave}", :green
154
+ do_start_slave(slave)
165
155
  end
166
156
 
167
157
  desc "change_master MASTER SLAVE FILE POSITION", "Execute CHANGE MASTER TO on the SLAVE."
168
158
  def change_master(master, slave, file, position)
169
- super
159
+ say "Changing master: master = #{master}, slave = #{slave}, file = #{file}, position = #{position}"
160
+ do_change_master(master, slave, :file => file, :position => position)
170
161
  end
171
162
 
172
- desc "crash INSTANCE", "Crash a running MySQL server."
163
+ desc "crash INSTANCE", "Crash a running server."
173
164
  def crash(instance)
174
- super
165
+ say "Crashing instance #{instance}", :red
166
+ do_crash(instance)
175
167
  end
176
168
 
177
169
  desc "repl_user INSTANCE", "Create the replication user account on a MySQL instance."
178
170
  def repl_user(instance)
179
- super
171
+ say "Creating replication account on instance #{instance}.", :green
172
+ do_repl_user(instance)
180
173
  end
181
174
 
182
175
  desc "cluster_user INSTANCE", "Create the cluster user account on a MySQL instance."
183
176
  def cluster_user(instance)
184
- super
177
+ say "Installing cluster user for instance #{instance}.", :green
178
+ do_cluster_user(instance)
185
179
  end
186
-
187
- desc "repl_status", "Show the status of replication."
180
+
181
+ desc "status", "Show the status of replication."
188
182
  method_option :continuous, :aliases => "-c", :type => :numeric,
189
183
  :desc => "Continuous output at specified interval (in seconds)."
190
184
  method_option :servers, :aliases => "-s", :type => :array,
191
185
  :desc => "Only check the status of given servers."
192
- def repl_status
186
+ def status
193
187
  todos = options[:servers] || all_live_instances
194
188
  unless todos.any?
195
189
  say "No Running Servers.", :blue
@@ -250,9 +244,31 @@ class Mysql < Thor
250
244
  def restore(slave, dumpfile = DEFAULT_DUMPFILE)
251
245
  do_restore(slave, dumpfile)
252
246
  end
253
-
247
+
248
+ #
249
+ # Setting Up Replication with Existing Data using the 'mysqldump' utility. The
250
+ # master has existing data.
251
+ #
252
+ desc "add_slave MASTER SLAVE", "Master has some data that is used to initialize the slave."
253
+ method_option :populate, :type => :boolean, :default => false
254
+ def add_slave(master, slave)
255
+ reset(slave)
256
+
257
+ if options[:populate]
258
+ invoke "utils:bench", [master, "/opt/MySQL/b2.properties"]
259
+ end
260
+ file, position = invoke "mysql:dump", [master]
261
+
262
+ # Slave is running, but it is not yet configured as a slave.
263
+ # Load slave from the dump file...
264
+ invoke "mysql:restore", [slave]
265
+
266
+ do_change_master(master, slave, :file => file, :position => position)
267
+ do_start_slave(slave)
268
+ end
269
+
254
270
  DEFAULT_MASTER = 1
255
-
271
+
256
272
  desc "bench [INSTANCE] [PROPS]", "Run the Tungsten Bristlecone benchmarker.
257
273
  The INSTANCE specifies the instance to perform all operations to, and PROPS
258
274
  is the properties file to use. The INSTANCE defaults to #{DEFAULT_MASTER} and
@@ -334,9 +350,8 @@ class Mysql < Thor
334
350
  msecs = options[:delay]
335
351
  sleep(msecs / 1000.0) if msecs > 0
336
352
  end
337
-
338
353
  end
339
-
354
+
340
355
  #
341
356
  # Setting Up Replication with New Master and Slaves.
342
357
  # Here, we stop all MySQL servers, remove the data directories, reinitialize
@@ -347,16 +362,16 @@ class Mysql < Thor
347
362
  "Set up a single master/slave replication pair from the very beginning."
348
363
  def repl_pair(master, slave)
349
364
  say "master is #{master}, slave is #{slave}", :green
350
- reset(master)
351
- reset(slave)
352
- cluster_user(master)
353
- repl_user(master)
365
+ do_reset(master)
366
+ do_reset(slave)
367
+ do_cluster_user(master)
368
+ do_repl_user(master)
354
369
  coordinates = get_coordinates(master)
355
370
  file = coordinates[:file]
356
371
  position = coordinates[:position]
357
372
  say "File is #{file}, Position is #{position}", :green
358
- change_master(master, slave, file, position)
359
- start_slave(slave)
373
+ do_change_master(master, slave, :file => file, :position => position)
374
+ do_start_slave(slave)
360
375
  end
361
376
 
362
377
  desc "repl_trio MASTER SLAVE1 SLAVE2",
@@ -365,48 +380,41 @@ class Mysql < Thor
365
380
  def repl_trio(master, slave1, slave2)
366
381
  say "master is #{master}, slaves are #{slave1} and #{slave2}", :green
367
382
  if options[:reset]
368
- reset(master)
369
- reset(slave1)
370
- reset(slave2)
371
- cluster_user(master)
372
- repl_user(master)
383
+ do_reset(master)
384
+ do_reset(slave1)
385
+ do_reset(slave2)
386
+ do_cluster_user(master)
387
+ do_repl_user(master)
373
388
  else
374
- restart(master)
375
- restart(slave1)
376
- restart(slave2)
389
+ do_restart(master)
390
+ do_restart(slave1)
391
+ do_restart(slave2)
377
392
  end
378
393
  coordinates = get_coordinates(master)
379
394
  file = coordinates[:file]
380
395
  position = coordinates[:position]
381
396
  say "File is #{file}, Position is #{position}", :green
382
- change_master(master, slave1, file, position)
383
- start_slave(slave1)
384
- change_master(master, slave2, file, position)
385
- start_slave(slave2)
397
+ do_change_master(master, slave1, :file => file, :position => position)
398
+ do_start_slave(slave1)
399
+ do_change_master(master, slave2, :file => file, :position => position)
400
+ do_start_slave(slave2)
386
401
  end
387
-
388
- #
389
- # Setting Up Replication with Existing Data using the 'mysqldump' utility. The
390
- # master has existing data.
391
- #
392
- desc "add_slave MASTER SLAVE", "Master has some data that is used to initialize the slave."
393
- method_option :populate, :type => :boolean, :default => false
394
- def add_slave(master, slave)
395
- reset(slave)
396
-
397
- if options[:populate]
398
- invoke "utils:bench", [master, "/opt/MySQL/b2.properties"]
402
+
403
+ desc "install sample config files to DIRECTORY", "After initial install, use these "
404
+ "as templates for configuration. The target directory must exist and be empty."
405
+ def install_sample_configs(dest_dir)
406
+ source_dir = File.join(Repctl::Config.const_get(:GEM_HOME), "config")
407
+ source_files = Dir.glob(File.join(source_dir, '*'))
408
+ if File.directory?(dest_dir)
409
+ if Dir.entries(dest_dir).size == 2 # directory is empty
410
+ FileUtils.copy(source_files, dest_dir)
411
+ else
412
+ say "Target directory must be empty.", :red
413
+ end
414
+ else
415
+ say "Config target #{to_dir} is not a directory.", :red
399
416
  end
400
- file, position = invoke "mysql:dump", [master]
401
-
402
- # Slave is running, but it is not yet configured as a slave.
403
- # Load slave from the dump file...
404
- invoke "mysql:restore", [slave]
405
-
406
- change_master(master, slave, file, position)
407
- start_slave(slave)
408
417
  end
409
-
410
418
  end
411
419
 
412
- Mysql.start
420
+ RepctlCmds.start
data/config/config.rb CHANGED
@@ -5,10 +5,12 @@ module Repctl
5
5
  # The location of the local MySQL installation.
6
6
  MYSQL_HOME = "/usr/local/mysql"
7
7
 
8
- ROOT_PASSWORD = "har526"
8
+ ROOT_PASSWORD = "graceling"
9
9
 
10
10
  SERVER_CONFIG = File.expand_path("../servers.yml", __FILE__)
11
11
 
12
+ GEM_HOME = File.expand_path("../..", __FILE__)
13
+
12
14
  # Define the directory where subdirectores for data will be created
13
15
  # for each MySQL server instance. This should agree with the
14
16
  # per server'data_dir' property in the servers.yml file, and it should
@@ -68,6 +68,26 @@ module Repctl
68
68
  include Repctl::Config
69
69
  include Servers
70
70
 
71
+ #
72
+ # Public methods are:
73
+ #
74
+ # do_secure_accounts
75
+ # do_start
76
+ # do_admin
77
+ # do_config
78
+ # do_crash
79
+ # do_change_master
80
+ # do_dump
81
+ # do_restore
82
+ # do_repl_user
83
+ # do_cluster_user
84
+ # do_create_widgets
85
+ # get_coordinates
86
+ # get_mysqld_pid
87
+ # get_slave_status
88
+ # run_mysql_query
89
+ #
90
+
71
91
  def do_secure_accounts(instance)
72
92
  client = Client.open(instance, :no_password => true)
73
93
  q1 = "UPDATE mysql.user SET Password = PASSWORD(\'#{ROOT_PASSWORD}\') where User = \'root\'"
@@ -147,12 +167,6 @@ module Repctl
147
167
  %x( cd #{MYSQL_HOME} && #{cmd} )
148
168
  end
149
169
 
150
- def do_status(instance)
151
-
152
- status = get_coordinates(instance)
153
- puts status
154
- end
155
-
156
170
  #
157
171
  # Treat the instance as a slave and
158
172
  # process the output of "SHOW SLAVE STATUS".
@@ -198,39 +212,6 @@ module Repctl
198
212
  end
199
213
  end
200
214
 
201
- def is_master?(instance)
202
- get_slave_coordinates(instance).empty?
203
- end
204
-
205
- def is_slave?(instance)
206
- !is_master?(instance)
207
- end
208
-
209
- # This is an example template to create commands to issue queries.
210
- def do_slave_status(instance)
211
- client = Client.open(instance)
212
- if client
213
- results = client.query("SHOW SLAVE STATUS")
214
- results.each_with_index do |index, line|
215
- puts "#{index}: #{line}"
216
- end
217
- else
218
- puts "Could not open connection to MySQL instance #{instance}."
219
- end
220
- rescue Mysql2::Error => e
221
- puts e.message
222
- ensure
223
- client.close if client
224
- end
225
-
226
- def find_masters()
227
- masters = []
228
- all_servers.each do |s|
229
- masters << s if is_master?(s)
230
- end
231
- masters
232
- end
233
-
234
215
  #
235
216
  # From http://dev.mysql.com/doc/refman/5.0/en/lock-tables.html:
236
217
  #
@@ -282,7 +263,6 @@ EOT
282
263
  ensure
283
264
  slave_connection.close if slave_connection
284
265
  end
285
-
286
266
  end
287
267
 
288
268
  def do_dump(instance, dumpfile)
@@ -312,38 +292,6 @@ EOT
312
292
  run_cmd(cmd, true)
313
293
  end
314
294
 
315
- #
316
- # Get the status of replication for the master and all slaves.
317
- # Return an array of hashes, each hash has the form:
318
- # {:instance => <instance id>, :error => <errrmsg>,
319
- # :master_file => <binlog-file-name>, :master_position => <binlog-position>,
320
- # :slave_file => <binlog-file-name>, :slave_position => <binlog-position>}
321
- #
322
- def do_slave_status(instance)
323
- instance ||= DEFAULT_MASTER
324
- locked = false
325
- client = Client.open(instance, :timeout => 5)
326
- if client
327
- client.query("FLUSH TABLES WITH READ LOCK")
328
- locked = true
329
- results = client.query("SHOW SLAVE STATUS")
330
- if results.first
331
- results.first.merge("Instance" => instance, "Error" => "Success")
332
- else
333
- {"Instance" => instance, "Error" => "MySQL server is not a slave."}
334
- end
335
- else
336
- {"Instance" => instance, "Error" => "Could not connect to MySQL server."}
337
- end
338
- rescue Mysql2::Error => e
339
- {:instance => instance, "Error" => e.message}
340
- ensure
341
- if client
342
- client.query("UNLOCK TABLES") if locked
343
- client.close
344
- end
345
- end
346
-
347
295
  # Get the master coordinates from a MySQL instance. Optionally,
348
296
  # run a block while holding the READ LOCK.
349
297
  def get_coordinates(instance)
@@ -375,30 +323,6 @@ EOT
375
323
  # coordinates
376
324
  end
377
325
 
378
- def get_slave_coordinates(instance)
379
- client = Client.open(instance)
380
- if client
381
- results = client.query("SHOW SLAVE STATUS")
382
- row = results.first
383
- if row
384
- {:file => row["Master_Log_File"], :position => row["Read_Master_Log_Pos"]}
385
- else
386
- {}
387
- end
388
- end
389
- ensure
390
- client.close if client
391
- end
392
-
393
- def stop_slave_io_thread(instance)
394
- client = Client.open(instance)
395
- if client
396
- client.query("STOP SLAVE IO_THREAD")
397
- end
398
- ensure
399
- client.close if client
400
- end
401
-
402
326
  def run_mysql_query(instance, cmd)
403
327
  client = Client.open(instance)
404
328
  if client
@@ -414,92 +338,6 @@ EOT
414
338
  client.close if client
415
339
  end
416
340
 
417
- def start_slave_io_thread(instance)
418
- client = Client.open(instance)
419
- if client
420
- client.query("START SLAVE IO_THREAD")
421
- end
422
- ensure
423
- client.close if client
424
- end
425
-
426
- def promote_slave_to_master(instance)
427
- client = Client.open(instance)
428
- if client
429
- client.query("STOP SLAVE")
430
- client.query("RESET MASTER")
431
- end
432
- ensure
433
- client.close if client
434
- end
435
-
436
- def drain_relay_log(instance)
437
- done = false
438
- stop_slave_io_thread(instance)
439
- client = Client.open(instance)
440
- if client
441
-
442
- # If the slave 'sql_thread' is not running, this will loop forever.
443
- while !done
444
- results = client.query("SHOW PROCESSLIST")
445
- results.each do |row|
446
- if row["State"] =~ /Slave has read all relay log/
447
- done = true
448
- puts "Slave has read all relay log."
449
- break
450
- end
451
- end
452
- puts "Waiting for slave to read relay log." unless done
453
- end
454
- else
455
- puts "Could not open connection to instance #{instance}."
456
- end
457
- ensure
458
- client.close if client
459
- end
460
-
461
- # 'master' is the new master
462
- # 'slaves' is contains the list of slaves, one of these may be the current master.
463
- def switch_master_to(master, slaves)
464
-
465
- slaves = Array(slaves)
466
-
467
- # Step 1. Make sure all slaves have completely processed their
468
- # Relay Log.
469
- slaves.each do |s|
470
- puts "Draining relay log for slave instance #{s}"
471
- drain_relay_log(s) if is_slave?(s)
472
- end
473
-
474
- # Step 2. For the slave being promoted to master, issue STOP SLAVE
475
- # and RESET MASTER.
476
- client = Client.open(master)
477
- client.query("STOP SLAVE")
478
- client.query("RESET MASTER")
479
- client.close
480
-
481
- # Step 3. Change the master for the other slaves (and the former master?)
482
- # XXX this is not complete -- what about the coordinates?
483
- master_server = server_for_instance(master)
484
- master_host = 'localhost' # should be master_server[:hostname]
485
- master_user = 'repl'
486
- master_password = 'har526'
487
- master_port = master_server[:port]
488
- slaves.each do |s|
489
- client = Client.open(s)
490
- cmd = <<-EOT
491
- CHANGE MASTER TO
492
- MASTER_HOST=\'#{master_host}\',
493
- MASTER_PORT=#{master_port},
494
- MASTER_USER=\'#{master_user}\',
495
- MASTER_PASSWORD=\'#{master_password}\'
496
- EOT
497
- client.query(cmd)
498
- client.query("START SLAVE")
499
- client.close
500
- end
501
- end
502
-
503
341
  def do_repl_user(instance)
504
342
  hostname = "127.0.0.1"
505
343
  client = Client.open(instance)
@@ -526,7 +364,6 @@ EOT
526
364
  client.close if client
527
365
  end
528
366
 
529
-
530
367
  def do_cluster_user(instance)
531
368
  client = Client.open(instance)
532
369
 
@@ -571,6 +408,16 @@ EOT
571
408
  client.close if client
572
409
  end
573
410
 
411
+ # Return the process ID (pid) for an instance.
412
+ def get_mysqld_pid(instance)
413
+ server = server_for_instance(instance)
414
+ pidfile = server['pid-file']
415
+ return nil unless File.exist?(pidfile)
416
+ Integer(File.open(pidfile, &:readline).strip)
417
+ end
418
+
419
+ private
420
+
574
421
  # This is an example template to create commands to issue queries.
575
422
  def template(instance)
576
423
  client = Client.open(instance)
@@ -586,8 +433,64 @@ EOT
586
433
  client.close if client
587
434
  end
588
435
 
589
- private
436
+ # 'master' is the new master
437
+ # 'slaves' is contains the list of slaves, one of these may be the current master.
438
+ def switch_master_to(master, slaves)
439
+
440
+ slaves = Array(slaves)
441
+
442
+ # Step 1. Make sure all slaves have completely processed their
443
+ # Relay Log.
444
+ slaves.each do |s|
445
+ puts "Draining relay log for slave instance #{s}"
446
+ drain_relay_log(s) if is_slave?(s)
447
+ end
448
+
449
+ # Step 2. For the slave being promoted to master, issue STOP SLAVE
450
+ # and RESET MASTER.
451
+ client = Client.open(master)
452
+ client.query("STOP SLAVE")
453
+ client.query("RESET MASTER")
454
+ client.close
455
+
456
+ # Step 3. Change the master for the other slaves (and the former master?)
457
+ # XXX this is not complete -- what about the coordinates?
458
+ master_server = server_for_instance(master)
459
+ master_host = 'localhost' # should be master_server[:hostname]
460
+ master_user = 'repl'
461
+ master_password = 'har526'
462
+ master_port = master_server[:port]
463
+ slaves.each do |s|
464
+ client = Client.open(s)
465
+ cmd = <<-EOT
466
+ CHANGE MASTER TO
467
+ MASTER_HOST=\'#{master_host}\',
468
+ MASTER_PORT=#{master_port},
469
+ MASTER_USER=\'#{master_user}\',
470
+ MASTER_PASSWORD=\'#{master_password}\'
471
+ EOT
472
+ client.query(cmd)
473
+ client.query("START SLAVE")
474
+ client.close
475
+ end
476
+ end
590
477
 
478
+ def is_master?(instance)
479
+ get_slave_coordinates(instance).empty?
480
+ end
481
+
482
+ def is_slave?(instance)
483
+ !is_master?(instance)
484
+ end
485
+
486
+ def find_masters()
487
+ masters = []
488
+ all_servers.each do |s|
489
+ masters << s if is_master?(s)
490
+ end
491
+ masters
492
+ end
493
+
591
494
  def run_cmd(cmd, verbose)
592
495
  puts cmd if verbose
593
496
  cmd += " > /dev/null 2>&1" unless verbose
@@ -600,14 +503,129 @@ EOT
600
503
  "FAIL: exit code is #{exit_code}"
601
504
  end
602
505
  end
603
-
604
- # Return the process ID (pid) for an instance.
605
- def get_mysqld_pid(instance)
606
- server = server_for_instance(instance)
607
- pidfile = server['pid-file']
608
- return nil unless File.exist?(pidfile)
609
- Integer(File.open(pidfile, &:readline).strip)
506
+
507
+ def stop_slave_io_thread(instance)
508
+ client = Client.open(instance)
509
+ if client
510
+ client.query("STOP SLAVE IO_THREAD")
511
+ end
512
+ ensure
513
+ client.close if client
514
+ end
515
+
516
+ # unused
517
+ def promote_slave_to_master(instance)
518
+ client = Client.open(instance)
519
+ if client
520
+ client.query("STOP SLAVE")
521
+ client.query("RESET MASTER")
522
+ end
523
+ ensure
524
+ client.close if client
525
+ end
526
+
527
+ def get_slave_coordinates(instance)
528
+ client = Client.open(instance)
529
+ if client
530
+ results = client.query("SHOW SLAVE STATUS")
531
+ row = results.first
532
+ if row
533
+ {:file => row["Master_Log_File"], :position => row["Read_Master_Log_Pos"]}
534
+ else
535
+ {}
536
+ end
537
+ end
538
+ ensure
539
+ client.close if client
540
+ end
541
+
542
+ # unused
543
+ def start_slave_io_thread(instance)
544
+ client = Client.open(instance)
545
+ if client
546
+ client.query("START SLAVE IO_THREAD")
547
+ end
548
+ ensure
549
+ client.close if client
610
550
  end
551
+
552
+ def drain_relay_log(instance)
553
+ done = false
554
+ stop_slave_io_thread(instance)
555
+ client = Client.open(instance)
556
+ if client
557
+
558
+ # If the slave 'sql_thread' is not running, this will loop forever.
559
+ while !done
560
+ results = client.query("SHOW PROCESSLIST")
561
+ results.each do |row|
562
+ if row["State"] =~ /Slave has read all relay log/
563
+ done = true
564
+ puts "Slave has read all relay log."
565
+ break
566
+ end
567
+ end
568
+ puts "Waiting for slave to read relay log." unless done
569
+ end
570
+ else
571
+ puts "Could not open connection to instance #{instance}."
572
+ end
573
+ ensure
574
+ client.close if client
575
+ end
576
+
577
+ =begin
578
+ # This is an example template to create commands to issue queries.
579
+ def do_slave_status(instance)
580
+ client = Client.open(instance)
581
+ if client
582
+ results = client.query("SHOW SLAVE STATUS")
583
+ results.each_with_index do |index, line|
584
+ puts "#{index}: #{line}"
585
+ end
586
+ else
587
+ puts "Could not open connection to MySQL instance #{instance}."
588
+ end
589
+ rescue Mysql2::Error => e
590
+ puts e.message
591
+ ensure
592
+ client.close if client
593
+ end
594
+ =end
595
+ #
596
+ # Get the status of replication for the master and all slaves.
597
+ # Return an array of hashes, each hash has the form:
598
+ # {:instance => <instance id>, :error => <errrmsg>,
599
+ # :master_file => <binlog-file-name>, :master_position => <binlog-position>,
600
+ # :slave_file => <binlog-file-name>, :slave_position => <binlog-position>}
601
+ #
602
+ def do_slave_status(instance)
603
+ instance ||= DEFAULT_MASTER
604
+ locked = false
605
+ client = Client.open(instance, :timeout => 5)
606
+ if client
607
+ client.query("FLUSH TABLES WITH READ LOCK")
608
+ locked = true
609
+ results = client.query("SHOW SLAVE STATUS")
610
+ if results.first
611
+ results.first.merge("Instance" => instance, "Error" => "Success")
612
+ else
613
+ {"Instance" => instance, "Error" => "MySQL server is not a slave."}
614
+ end
615
+ else
616
+ {"Instance" => instance, "Error" => "Could not connect to MySQL server."}
617
+ end
618
+ rescue Mysql2::Error => e
619
+ {:instance => instance, "Error" => e.message}
620
+ ensure
621
+ if client
622
+ client.query("UNLOCK TABLES") if locked
623
+ client.close
624
+ end
625
+ end
626
+
627
+
628
+
611
629
  end
612
630
  end
613
631
 
@@ -1,3 +1,3 @@
1
1
  module Repctl
2
- VERSION = "0.0.2"
2
+ VERSION = "0.0.3"
3
3
  end
data/lib/repctl.rb CHANGED
@@ -1,4 +1,8 @@
1
- require File.expand_path('../../config/config', __FILE__)
1
+ # The compiled-in default is not generally very useful.
2
+ config_dir = ENV["REPCTL_CONFIG_DIR"] ||
3
+ File.expand_path('../../config', __FILE__)
4
+
5
+ require File.join(config_dir, 'config')
2
6
 
3
7
  require "repctl/version"
4
8
  require "repctl/servers"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: repctl
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.3
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -13,7 +13,7 @@ date: 2012-01-26 00:00:00.000000000Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rspec
16
- requirement: &205152340 !ruby/object:Gem::Requirement
16
+ requirement: &55282760 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ! '>='
@@ -21,10 +21,10 @@ dependencies:
21
21
  version: '0'
22
22
  type: :development
23
23
  prerelease: false
24
- version_requirements: *205152340
24
+ version_requirements: *55282760
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: thor
27
- requirement: &205150660 !ruby/object:Gem::Requirement
27
+ requirement: &55282340 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ! '>='
@@ -32,10 +32,10 @@ dependencies:
32
32
  version: '0'
33
33
  type: :runtime
34
34
  prerelease: false
35
- version_requirements: *205150660
35
+ version_requirements: *55282340
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: mysql2
38
- requirement: &205149240 !ruby/object:Gem::Requirement
38
+ requirement: &55281920 !ruby/object:Gem::Requirement
39
39
  none: false
40
40
  requirements:
41
41
  - - ! '>='
@@ -43,7 +43,7 @@ dependencies:
43
43
  version: '0'
44
44
  type: :runtime
45
45
  prerelease: false
46
- version_requirements: *205149240
46
+ version_requirements: *55281920
47
47
  description: Ruby gem with Thor script to manage MySQL and PostgreSQL replication
48
48
  email:
49
49
  - lydianblues@gmail.com