#!/usr/bin/env ruby require 'bundler/setup' require 'mysql2' require 'socket' require 'pp' Thread.abort_on_exception = false $old_master, $new_master, $username, $password = *ARGV unless $old_master && $new_master && $username && $password puts "Usage: master_cut OLD_MASTER NEW_MASTER USERNAME PASSWORD" exit end def open_cx(host) host, port = host.split(":") port = port.to_i if port Mysql2::Client.new(:host => host, :username => $username, :password => $password, :port => port) end def set_rw(cx) cx.query("SET GLOBAL READ_ONLY=0") end def set_ro(cx) cx.query("SET GLOBAL READ_ONLY=1") end $swapped_ok = false def fail(reason) puts "Failed preflight check: #{reason}" exit false end def preflight_check cx = open_cx($old_master) rw = cx.query("select @@read_only as read_only").first['read_only'] fail("old-master #{$old_master} is read-only!") if rw != 0 slave_cx = open_cx($new_master) rw = slave_cx.query("select @@read_only as read_only").first['read_only'] fail("new-master #{$old_master} is read-write!") if rw != 1 slave_info = slave_cx.query("show slave status").first fail("slave is stopped!") unless slave_info['Slave_IO_Running'] == 'Yes' && slave_info['Slave_SQL_Running'] == 'Yes' fail("slave is delayed") if slave_info['Seconds_Behind_Master'].nil? || slave_info['Seconds_Behind_Master'] > 0 master_ip, slave_master_ip = [$old_master, slave_info['Master_Host']].map do |h| h = h.split(':').first Socket.gethostbyname(h)[3].unpack("CCCC") end if master_ip != slave_master_ip fail("slave does not appear to be replicating off master! (master: #{master_ip.join('.')}, slave's master: #{slave_master_ip.join('.')})") end end def process_kill_thread Thread.new do cx = open_cx($old_master) sleep 5 while !$swapped_ok my_id = cx.query("SELECT CONNECTION_ID() as id").first['id'] processlist = cx.query("show processlist") processlist.each do |process| next if process['Info'] =~ /SET GLOBAL READ_ONLY/ next if process['Id'].to_i == my_id.to_i puts "killing #{process}" cx.query("kill #{process['Id']}") end sleep 0.1 end end end def swap_thread Thread.new do master = open_cx($old_master) slave = open_cx($new_master) set_ro(master) slave.query("slave stop") new_master_info = slave.query("show master status").first set_rw(slave) $swapped_ok = true puts "Swapped #{$old_master} and #{$new_master}" puts "New master information at time of swap: " pp new_master_info exit end end preflight_check threads = [] threads << process_kill_thread threads << swap_thread threads.each(&:join)