reptile 0.0.1
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/History.txt +4 -0
- data/Manifest.txt +20 -0
- data/PostInstall.txt +5 -0
- data/README.rdoc +36 -0
- data/Rakefile +38 -0
- data/bin/replication_status +69 -0
- data/lib/reptile/databases.rb +51 -0
- data/lib/reptile/delta_monitor.rb +86 -0
- data/lib/reptile/dtd.sql +18 -0
- data/lib/reptile/heartbeat.rb +99 -0
- data/lib/reptile/replication_monitor.rb +224 -0
- data/lib/reptile/runner.rb +116 -0
- data/lib/reptile/status.rb +79 -0
- data/lib/reptile/users.rb +64 -0
- data/lib/reptile.rb +23 -0
- data/script/console +10 -0
- data/script/destroy +14 -0
- data/script/generate +14 -0
- data/test/test_helper.rb +3 -0
- data/test/test_reptile.rb +11 -0
- metadata +97 -0
data/History.txt
ADDED
data/Manifest.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
History.txt
|
2
|
+
Manifest.txt
|
3
|
+
PostInstall.txt
|
4
|
+
README.rdoc
|
5
|
+
Rakefile
|
6
|
+
bin/replication_status
|
7
|
+
lib/reptile.rb
|
8
|
+
lib/reptile/databases.rb
|
9
|
+
lib/reptile/delta_monitor.rb
|
10
|
+
lib/reptile/dtd.sql
|
11
|
+
lib/reptile/heartbeat.rb
|
12
|
+
lib/reptile/replication_monitor.rb
|
13
|
+
lib/reptile/runner.rb
|
14
|
+
lib/reptile/status.rb
|
15
|
+
lib/reptile/users.rb
|
16
|
+
script/console
|
17
|
+
script/destroy
|
18
|
+
script/generate
|
19
|
+
test/test_helper.rb
|
20
|
+
test/test_reptile.rb
|
data/PostInstall.txt
ADDED
data/README.rdoc
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
= reptile
|
2
|
+
|
3
|
+
Reptile is an easy to use utility that will monitor your MySQL replication, so you can forget about it and focus on the good stuff. It provides a utility for generate replication reports, and can email if replication appears to be failing.
|
4
|
+
|
5
|
+
== REQUIREMENTS:
|
6
|
+
|
7
|
+
A mysql replication setup.
|
8
|
+
|
9
|
+
== INSTALL:
|
10
|
+
|
11
|
+
sudo gem install reptile
|
12
|
+
|
13
|
+
== LICENSE:
|
14
|
+
|
15
|
+
(The MIT License)
|
16
|
+
|
17
|
+
Copyright (c) 2009 Nick Stielau
|
18
|
+
|
19
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
20
|
+
a copy of this software and associated documentation files (the
|
21
|
+
'Software'), to deal in the Software without restriction, including
|
22
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
23
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
24
|
+
permit persons to whom the Software is furnished to do so, subject to
|
25
|
+
the following conditions:
|
26
|
+
|
27
|
+
The above copyright notice and this permission notice shall be
|
28
|
+
included in all copies or substantial portions of the Software.
|
29
|
+
|
30
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
31
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
32
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
33
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
34
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
35
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
36
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'rubygems' unless ENV['NO_RUBYGEMS']
|
2
|
+
%w[rake rake/clean fileutils newgem rubigen].each { |f| require f }
|
3
|
+
require File.dirname(__FILE__) + '/lib/reptile'
|
4
|
+
|
5
|
+
# Generate all the Rake tasks
|
6
|
+
# Run 'rake -T' to see list of generated tasks (from gem root directory)
|
7
|
+
$hoe = Hoe.new('reptile', Reptile::VERSION) do |p|
|
8
|
+
p.developer('FIXME full name', 'FIXME email')
|
9
|
+
p.changes = p.paragraphs_of("History.txt", 0..1).join("\n\n")
|
10
|
+
p.post_install_message = 'PostInstall.txt'
|
11
|
+
p.rubyforge_name = p.name
|
12
|
+
p.bin_files = ["bin/replication_status"]
|
13
|
+
p.extra_dev_deps = [
|
14
|
+
['newgem', ">= #{::Newgem::VERSION}"]
|
15
|
+
]
|
16
|
+
|
17
|
+
p.clean_globs |= %w[**/.DS_Store tmp *.log]
|
18
|
+
path = (p.rubyforge_name == p.name) ? p.rubyforge_name : "\#{p.rubyforge_name}/\#{p.name}"
|
19
|
+
p.remote_rdoc_dir = File.join(path.gsub(/^#{p.rubyforge_name}\/?/,''), 'rdoc')
|
20
|
+
p.rsync_args = '-av --delete --ignore-errors'
|
21
|
+
end
|
22
|
+
|
23
|
+
desc "Upload current documentation to Rubyforge"
|
24
|
+
task :upload_docs => [:redocs] do
|
25
|
+
sh "scp -r doc/* nstielau@rubyforge.org:/var/www/gforge-projects/reptile/doc/"
|
26
|
+
end
|
27
|
+
|
28
|
+
desc "Upload current documentation to Rubyforge"
|
29
|
+
task :upload_site do
|
30
|
+
#webgen && scp -r output/* nstielau@rubyforge.org:/var/www/gforge-projects/reptile/
|
31
|
+
sh "scp -r site/* nstielau@rubyforge.org:/var/www/gforge-projects/reptile/"
|
32
|
+
end
|
33
|
+
|
34
|
+
require 'newgem/tasks' # load /tasks/*.rake
|
35
|
+
Dir['tasks/**/*.rake'].each { |t| load t }
|
36
|
+
|
37
|
+
# TODO - want other tests/tasks run by default? Add them to the list
|
38
|
+
# task :default => [:spec, :features]
|
@@ -0,0 +1,69 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'optparse'
|
4
|
+
require 'reptile'
|
5
|
+
|
6
|
+
commands = []
|
7
|
+
|
8
|
+
OptionParser.new do |opts|
|
9
|
+
opts.banner = "Usage: #{File.basename($0)} [path_to_config_file]"
|
10
|
+
|
11
|
+
opts.on("-h", "--help", "Displays this help info") do
|
12
|
+
puts opts
|
13
|
+
exit 0
|
14
|
+
end
|
15
|
+
|
16
|
+
opts.on("-s", "--status", "Displays the slave status") do
|
17
|
+
commands << 'check_slaves'
|
18
|
+
end
|
19
|
+
|
20
|
+
opts.on("-d", "--diff", "Checks the row count difference between master and slave") do
|
21
|
+
commands << 'diff_tables'
|
22
|
+
end
|
23
|
+
|
24
|
+
opts.on("-r", "--report", "Sends a report email") do
|
25
|
+
commands << 'report'
|
26
|
+
end
|
27
|
+
|
28
|
+
opts.on("-b", "--heartbeat", "Checks the heartbeat timestamp difference between master and slave") do
|
29
|
+
commands << 'heartbeat'
|
30
|
+
end
|
31
|
+
|
32
|
+
opts.on("-x", "--stop_slaves", "Stops all slaves") do
|
33
|
+
commands << 'stop_slaves'
|
34
|
+
end
|
35
|
+
|
36
|
+
opts.on("-g", "--start_slaves", "Starts all slaves") do
|
37
|
+
commands << 'start_slaves'
|
38
|
+
end
|
39
|
+
|
40
|
+
begin
|
41
|
+
opts.parse!(ARGV)
|
42
|
+
rescue OptionParser::ParseError => e
|
43
|
+
warn e.message
|
44
|
+
puts opts
|
45
|
+
exit 1
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
config_file = 'replication.yml'
|
50
|
+
|
51
|
+
if ARGV.empty? && !File.exists?(config_file)
|
52
|
+
abort "Please specify the directory containing the '#{config_file}' file, e.g. `#{File.basename($0)} ~/repl'"
|
53
|
+
elsif !ARGV.empty? && !File.exists?(ARGV.first)
|
54
|
+
abort "`#{ARGV.first}' does not exist."
|
55
|
+
elsif !ARGV.empty? && !File.directory?(ARGV.first)
|
56
|
+
abort "`#{ARGV.first}' is not a directory."
|
57
|
+
elsif !ARGV.empty? && ARGV.length > 1
|
58
|
+
abort "Too many arguments; please specify only the directory to #{File.basename($0)}."
|
59
|
+
end
|
60
|
+
|
61
|
+
Reptile::ReplicationMonitor.load_config_file(ARGV.first.nil? ? config_file : "#{ARGV.first}/#{config_file}")
|
62
|
+
|
63
|
+
if (commands.include?('start_slaves') || commands.include?('stop_slaves'))
|
64
|
+
Reptile::Runner.send(commands[0])
|
65
|
+
else
|
66
|
+
(commands.empty? ? ['check_slaves', 'heartbeat', 'diff_tables'] : commands).each do |command|
|
67
|
+
Reptile::ReplicationMonitor.send(command)
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Reptile
|
2
|
+
# The Databases class stores information about different databases, including the config settings
|
3
|
+
# for the master and slave of each particular database.
|
4
|
+
class Databases
|
5
|
+
attr :databases
|
6
|
+
|
7
|
+
def initialize(databases)
|
8
|
+
@databases = databases
|
9
|
+
end
|
10
|
+
|
11
|
+
# returns an array of the master names
|
12
|
+
def masters
|
13
|
+
@master_configs ||= get_masters
|
14
|
+
end
|
15
|
+
|
16
|
+
# returns an array of the slave names
|
17
|
+
def slaves
|
18
|
+
@slave_configs ||= get_slaves
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def get_masters
|
24
|
+
masters = databases.dup
|
25
|
+
masters.each_key{|key| masters.delete(key) if masters[key]['master'].nil? }
|
26
|
+
masters.each_key{|key| masters[key] = masters[key]['master'] }
|
27
|
+
masters
|
28
|
+
end
|
29
|
+
|
30
|
+
# TODO: make private
|
31
|
+
def get_slaves
|
32
|
+
dbs = databases.dup
|
33
|
+
dbs.each_key{|name| dbs.delete(key) if dbs[name]['slave'].nil? }
|
34
|
+
dbs.each_key{|name| dbs[name] = dbs[name]['slave'] }
|
35
|
+
slaves = dbs
|
36
|
+
end
|
37
|
+
|
38
|
+
# Tries to establish a database connection, and returns that connection.
|
39
|
+
# Dumps configs on error
|
40
|
+
def self.connect(configs)
|
41
|
+
ActiveRecord::Base.establish_connection(configs)
|
42
|
+
ActiveRecord::Base.connection
|
43
|
+
rescue Exception => e
|
44
|
+
puts "****"
|
45
|
+
puts "Error connecting to database: #{e}"
|
46
|
+
puts "****"
|
47
|
+
puts YAML::dump(configs)
|
48
|
+
exit 1
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module Reptile
|
2
|
+
# This monitor compares the row counts for each table for each master and slave.
|
3
|
+
class DeltaMonitor
|
4
|
+
# Set the user settings for a user that has global SELECT privilidgess
|
5
|
+
def self.user=(user_settings)
|
6
|
+
@user = user_settings
|
7
|
+
end
|
8
|
+
|
9
|
+
# The user settings for a user that has global select privilidgess
|
10
|
+
def self.user
|
11
|
+
raise "You need to specify a user!" if @user.nil?
|
12
|
+
@user
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.open_log()
|
16
|
+
logFile = 'db_delta.log'
|
17
|
+
@logFileObj = File.open(logFile, "a")
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.log(msg)
|
21
|
+
open_log if @logFileObj.nil?
|
22
|
+
puts msg
|
23
|
+
@logFileObj.puts msg
|
24
|
+
end
|
25
|
+
|
26
|
+
# Retrieve the active database connection. Nil of none exists.
|
27
|
+
def self.connection
|
28
|
+
ActiveRecord::Base.connection
|
29
|
+
end
|
30
|
+
|
31
|
+
# Compares the row counts for master tables and slave tables
|
32
|
+
# Returns a hash of TABLE_NAME => ROW COUNT DELTAs
|
33
|
+
def self.diff(db_name, master_configs, slave_configs)
|
34
|
+
ActiveRecord::Base.establish_connection(slave_configs.merge(user))
|
35
|
+
slave_counts = get_table_counts
|
36
|
+
|
37
|
+
ActiveRecord::Base.establish_connection(master_configs.merge(user))
|
38
|
+
master_counts = get_table_counts
|
39
|
+
|
40
|
+
deltas= {}
|
41
|
+
master_counts.each do |table, master_count|
|
42
|
+
delta = master_count.first.to_i - slave_counts[table].first.to_i
|
43
|
+
deltas[table] = delta
|
44
|
+
end
|
45
|
+
|
46
|
+
print_deltas(db_name, deltas, master_configs)
|
47
|
+
|
48
|
+
deltas
|
49
|
+
end
|
50
|
+
|
51
|
+
# Prints stats about the differences in number of rows between the master and slave
|
52
|
+
def self.print_deltas(db_name, deltas, configs)
|
53
|
+
non_zero_deltas = deltas.select{|table, delta| not delta.zero?}
|
54
|
+
if non_zero_deltas.size.zero?
|
55
|
+
log "Replication counts A-OK for #{db_name} on #{configs['host']} @ #{Time.now}"
|
56
|
+
else
|
57
|
+
log "Replication Row Count Deltas for #{db_name} on #{configs['host']} @ #{Time.now}"
|
58
|
+
log "There #{non_zero_deltas.size > 1 ? 'are' : 'is'} #{non_zero_deltas.size} #{non_zero_deltas.size > 1 ? 'deltas' : 'delta'}"
|
59
|
+
non_zero_deltas.each do |table, delta|
|
60
|
+
log " #{table} table: #{delta}" unless delta.zero?
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# Returns an array of strings containing the table names
|
66
|
+
# for the current database.
|
67
|
+
def self.get_tables
|
68
|
+
tables = []
|
69
|
+
connection.execute('SHOW TABLES').each { |row| tables << row }
|
70
|
+
tables
|
71
|
+
end
|
72
|
+
|
73
|
+
# Returns a hash of TABLE_NAME => # Rows for all tables in current db
|
74
|
+
def self.get_table_counts
|
75
|
+
tables = get_tables
|
76
|
+
|
77
|
+
tables_w_count = {}
|
78
|
+
tables.each do |table|
|
79
|
+
connection.execute("SELECT COUNT(*) FROM #{table}").each do |table_count|
|
80
|
+
tables_w_count["#{table}"] = table_count
|
81
|
+
end
|
82
|
+
end
|
83
|
+
tables_w_count
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
data/lib/reptile/dtd.sql
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
// Replace 'REPLICATION_USER', 'MONITORING_BOX', 'REPLICATION_PASS', 'READ_ONLY_PASS', 'READ_ONLY_USER', 'HEARTBEAT_USER' and 'HEARTBEAT_PASS', then execute this SQL as root on the master, slave, and monitoring server
|
2
|
+
|
3
|
+
GRANT REPLICATION SLAVE, REPLICATION CLIENT, SUPER ON *.* TO 'REPLICATION_USER'@"localhost" IDENTIFIED BY 'REPLICATION_PASS';
|
4
|
+
GRANT REPLICATION SLAVE, REPLICATION CLIENT, SUPER ON *.* TO 'REPLICATION_USER'@"MONITORING_BOX" IDENTIFIED BY 'REPLICATION_PASS';
|
5
|
+
|
6
|
+
GRANT SELECT, REPLICATION CLIENT ON *.* TO 'READ_ONLY_USER'@"localhost" IDENTIFIED BY 'READ_ONLY_PASS';
|
7
|
+
GRANT SELECT, REPLICATION CLIENT ON *.* TO 'READ_ONLY_USER'@"MONITORING_BOX" IDENTIFIED BY 'READ_ONLY_PASS';
|
8
|
+
|
9
|
+
GRANT SELECT, INSERT, UPDATE, ALTER ON replication_monitor.* TO 'HEARTBEAT_USER'@"localhost" IDENTIFIED BY 'HEARTBEAT_PASS';
|
10
|
+
GRANT SELECT, INSERT, UPDATE, ALTER ON replication_monitor.* TO 'HEARTBEAT_USER'@"MONITORING_BOX" IDENTIFIED BY 'HEARTBEAT_PASS';
|
11
|
+
|
12
|
+
CREATE DATABASE replication_monitor;
|
13
|
+
|
14
|
+
CREATE TABLE replication_monitor.heartbeats (
|
15
|
+
unix_time INTEGER NOT NULL,
|
16
|
+
db_time TIMESTAMP NOT NULL,
|
17
|
+
INDEX time_idx(unix_time)
|
18
|
+
)
|
@@ -0,0 +1,99 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
|
3
|
+
module Reptile
|
4
|
+
# MySQL DTD for setting up Heartbeats. Change HEARTBEAT_USER, HEARTBEAT_PASS, and MONITORING_BOX
|
5
|
+
# to ip of the monitoring server.
|
6
|
+
#
|
7
|
+
# GRANT SELECT, INSERT, UPDATE, ALTER ON replication_monitor.*
|
8
|
+
# TO 'HEARTBEAT_USER'@"localhost" IDENTIFIED BY 'HEARTBEAT_PASS'; GRANT SELECT, INSERT, UPDATE,
|
9
|
+
# ALTER ON replication_monitor.* TO 'HEARTBEAT_USER'@"MONITORING_BOX" IDENTIFIED BY 'HEARTBEAT_PASS';
|
10
|
+
#
|
11
|
+
# CREATE DATABASE replication_monitor;
|
12
|
+
#
|
13
|
+
# CREATE TABLE replication_monitor.heartbeats (
|
14
|
+
# unix_time INTEGER NOT NULL,
|
15
|
+
# db_time TIMESTAMP NOT NULL,
|
16
|
+
# INDEX time_idx(unix_time)
|
17
|
+
# )
|
18
|
+
#
|
19
|
+
class Heartbeat < ActiveRecord::Base
|
20
|
+
|
21
|
+
# Set the default connection settings for writing/reading heartbeats.
|
22
|
+
# These will be merged with the per-database settings passed to <code>connect</code>.
|
23
|
+
def self.user=(default_configs)
|
24
|
+
@user = default_configs
|
25
|
+
end
|
26
|
+
|
27
|
+
# The default connection settings which override per-database settings.
|
28
|
+
def self.user
|
29
|
+
@user ||= {}
|
30
|
+
end
|
31
|
+
|
32
|
+
# Merge the connection settings in the configs parameter with HeartBeat defaults.
|
33
|
+
def self.connect(configs)
|
34
|
+
ReplicationMonitor.connect(configs.merge(user))
|
35
|
+
end
|
36
|
+
|
37
|
+
# Write a heartbeat.
|
38
|
+
# Thump thump.
|
39
|
+
def self.write(name, configs)
|
40
|
+
connect(configs)
|
41
|
+
heartbeat = Heartbeat.create(:unix_time => Time.now.to_i, :db_time => "NOW()")
|
42
|
+
log "Wrote heartbeat to #{name} at #{Time.at(heartbeat.unix_time)}"
|
43
|
+
end
|
44
|
+
|
45
|
+
# Read the most recent heartbeat and return the delay in seconds, or nil if no heartbeat are found.
|
46
|
+
def self.read(name, configs)
|
47
|
+
connect(configs)
|
48
|
+
|
49
|
+
current_time = Time.now
|
50
|
+
|
51
|
+
delay = nil
|
52
|
+
heartbeat = Heartbeat.find(:first, :order => 'db_time DESC')
|
53
|
+
|
54
|
+
# No heartbeats at all!
|
55
|
+
if heartbeat.nil?
|
56
|
+
log "No heartbeats found on #{name} at #{Time.now}"
|
57
|
+
return nil;
|
58
|
+
end
|
59
|
+
|
60
|
+
# Not sure why we have both, (one is easier to read?).
|
61
|
+
# Use one or the other to calculate delay...
|
62
|
+
delay = (Time.now - Time.at(heartbeat.unix_time)).round
|
63
|
+
#delay = (Time.now - heartbeat.db_time)
|
64
|
+
|
65
|
+
log "Read heartbeat from #{name} at #{Time.at(heartbeat.unix_time)}. The delay is #{strfdelay(delay)}"
|
66
|
+
|
67
|
+
delay
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
# Open the 'heartbeat.log' file.
|
73
|
+
def self.open_log
|
74
|
+
logFile = 'heartbeat.log'
|
75
|
+
@logFileObj = File.open(logFile, "a")
|
76
|
+
end
|
77
|
+
|
78
|
+
# Log a message, both to the file and standard out.
|
79
|
+
def self.log(msg)
|
80
|
+
open_log if @logFileObj.nil?
|
81
|
+
puts msg
|
82
|
+
@logFileObj.puts msg
|
83
|
+
end
|
84
|
+
|
85
|
+
# Format the delay (in seconds) as a human-readable string.
|
86
|
+
def self.strfdelay(delay)
|
87
|
+
seconds = delay % 60
|
88
|
+
minutes = delay / 60 % 60
|
89
|
+
hours = delay / 60 / 60
|
90
|
+
delay_str = ""
|
91
|
+
delay_str << "#{hours} hours" if hours > 0
|
92
|
+
delay_str << " " if !hours.zero? && (minutes > 0 || seconds > 0)
|
93
|
+
delay_str << "#{minutes} minutes" if minutes > 0
|
94
|
+
delay_str << " " if (!minutes.zero? || !hours.zero?) && seconds > 0
|
95
|
+
delay_str << "#{seconds} seconds" unless seconds.zero?
|
96
|
+
delay_str
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,224 @@
|
|
1
|
+
module Reptile
|
2
|
+
class ReplicationMonitor
|
3
|
+
# Attempts to load the replication.yml configuration file.
|
4
|
+
def self.load_config_file(databases_file)
|
5
|
+
@databases_file = databases_file
|
6
|
+
yaml = YAML::load(File.read(@databases_file))
|
7
|
+
@configs = yaml.delete('config')
|
8
|
+
@users = Users.new(yaml.delete('users'))
|
9
|
+
@databases = Databases.new(yaml)
|
10
|
+
|
11
|
+
Heartbeat.user = users.heartbeat_user
|
12
|
+
Runner.user = users.replication_user
|
13
|
+
Status.user = users.replication_user
|
14
|
+
DeltaMonitor.user = users.ro_user
|
15
|
+
Runner.databases = databases
|
16
|
+
|
17
|
+
raise "Please specify a delay theshold 'delay_threshold_secs: 360'" if @configs['delay_threshold_secs'].nil?
|
18
|
+
raise "Please specify a row delta theshold 'row_difference_threshold: 10'" if @configs['row_difference_threshold'].nil?
|
19
|
+
|
20
|
+
rescue Errno::EACCES => e
|
21
|
+
puts "Unable to open config file: Permission Denied"
|
22
|
+
end
|
23
|
+
|
24
|
+
# Returns the configs from the replication.yml file
|
25
|
+
def self.configs
|
26
|
+
@configs
|
27
|
+
end
|
28
|
+
|
29
|
+
# Returns the databases from the yml file.
|
30
|
+
def self.databases
|
31
|
+
@databases
|
32
|
+
end
|
33
|
+
|
34
|
+
# Returns the +Users+ loaded from the replication.yml file
|
35
|
+
def self.users
|
36
|
+
@users
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.diff_tables
|
40
|
+
unsynced_dbs = 0
|
41
|
+
|
42
|
+
databases.each_pair do |name, roles|
|
43
|
+
master, slave = roles['master'], roles['slave']
|
44
|
+
deltas = DeltaMonitor.diff(name, master, slave)
|
45
|
+
|
46
|
+
egregious_deltas = deltas.select{|table, delta| delta > configs['row_difference_threshold'] }
|
47
|
+
if egregious_deltas.size > 0
|
48
|
+
queue_replication_warning :host => master[:host], :database => master[:database], :deltas => egregious_deltas, :noticed_at => Time.now
|
49
|
+
unsynced_dbs += 1
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
unsynced_dbs.zero?
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.heartbeat
|
57
|
+
databases.each_key do |name|
|
58
|
+
Heartbeat.write(name, databases[name]['master'])
|
59
|
+
end
|
60
|
+
|
61
|
+
overdue_slaves = 0
|
62
|
+
|
63
|
+
databases.each_key do |name|
|
64
|
+
db_configs = databases[name]['slave']
|
65
|
+
delay = Heartbeat.read(name, db_configs)
|
66
|
+
if delay.nil?
|
67
|
+
queue_replication_warning :host => name,
|
68
|
+
:database => configs[:database],
|
69
|
+
:general_error => "Error: No Heartbeats found.",
|
70
|
+
:noticed_at => Time.now
|
71
|
+
overdue_slaves += 1
|
72
|
+
elsif delay > configs['delay_threshold_secs']
|
73
|
+
queue_replication_warning :host => name,
|
74
|
+
:database => configs[:database],
|
75
|
+
:delay => Heartbeat.strfdelay(delay),
|
76
|
+
:noticed_at => Time.now
|
77
|
+
overdue_slaves += 1
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
overdue_slaves.zero?
|
82
|
+
end
|
83
|
+
|
84
|
+
# Checks the status of each slave.
|
85
|
+
def self.check_slaves
|
86
|
+
databases.slaves.each do |slave_name, slave_configs|
|
87
|
+
status = Status.check_slave_status(slave_name, slave_configs)
|
88
|
+
if status != Status.const_get(:RUNNING)
|
89
|
+
queue_replication_warning :host => name,
|
90
|
+
:database => configs[:database],
|
91
|
+
:status_error => Status.get_error_message(status),
|
92
|
+
:noticed_at => Time.now
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def self.queue_replication_warning(options)
|
98
|
+
email = OpenStruct.new
|
99
|
+
email.recipients = get_recipients
|
100
|
+
email.subject = "A replication error occured on #{options[:host]} at #{Time.now}"
|
101
|
+
email.body = ''
|
102
|
+
|
103
|
+
if options[:delay]
|
104
|
+
email.body += "There was a #{delay} second replication latency, which is greater than the allowed latency of #{configs['delay_threshold_secs']} seconds"
|
105
|
+
elsif options[:deltas]
|
106
|
+
email.body += "The following tables have master/slave row count difference greater than the allowed #{configs['row_difference_threshold']}\n\n"
|
107
|
+
options[:deltas].each do |table, delta|
|
108
|
+
email.body += " table '#{table}' was off by #{delta} rows\n"
|
109
|
+
end
|
110
|
+
elsif options[:status_error]
|
111
|
+
email.body += " MySQL Status message: #{options[:status_error]}"
|
112
|
+
elsif options[:general_error]
|
113
|
+
email.body += " Error: #{options[:general_error]}"
|
114
|
+
end
|
115
|
+
|
116
|
+
email.body += "\n"
|
117
|
+
email.body += " Server: #{options[:host]}\n"
|
118
|
+
email.body += " Databse: #{options[:database]}\n"
|
119
|
+
|
120
|
+
send_email(email)
|
121
|
+
end
|
122
|
+
|
123
|
+
# Gets the 'email_to' value from the 'configs' section of the replication.yml file
|
124
|
+
def self.get_recipients
|
125
|
+
configs['email_to']
|
126
|
+
end
|
127
|
+
|
128
|
+
# Gets the 'email_from' value from the 'configs' section of the replication.yml file
|
129
|
+
def self.get_sender
|
130
|
+
configs['email_from']
|
131
|
+
end
|
132
|
+
|
133
|
+
def self.report
|
134
|
+
email = OpenStruct.new
|
135
|
+
email.recipients = get_recipients
|
136
|
+
email.sender = get_sender
|
137
|
+
raise "Please specify report recipients 'email_to: user@address.com'" if email.recipients.nil?
|
138
|
+
raise "Please specify report recipients 'email_from: user@address.com'" if email.sender.nil?
|
139
|
+
|
140
|
+
email.subject = "Daily Replication Report for #{Time.now.strftime('%D')}"
|
141
|
+
|
142
|
+
puts "Generating report email"
|
143
|
+
|
144
|
+
old_stdout = $stdout
|
145
|
+
out = StringIO.new
|
146
|
+
$stdout = out
|
147
|
+
begin
|
148
|
+
puts " Checking slave status"
|
149
|
+
puts
|
150
|
+
self.check_slaves
|
151
|
+
puts
|
152
|
+
puts
|
153
|
+
puts " Checking table row counts"
|
154
|
+
puts
|
155
|
+
puts "The row count difference threshold is #{configs['row_difference_threshold']} rows"
|
156
|
+
puts
|
157
|
+
self.diff_tables
|
158
|
+
puts
|
159
|
+
puts
|
160
|
+
puts " Checking replication heartbeat"
|
161
|
+
puts
|
162
|
+
puts "The heartbeat latency threshold is #{configs['delay_threshold_secs']} seconds"
|
163
|
+
puts
|
164
|
+
self.heartbeat
|
165
|
+
ensure
|
166
|
+
$stdout = old_stdout
|
167
|
+
end
|
168
|
+
email.body = out.string
|
169
|
+
|
170
|
+
puts "Sending report email"
|
171
|
+
|
172
|
+
send_email(email)
|
173
|
+
|
174
|
+
puts "Report sent to #{get_recipients}"
|
175
|
+
end
|
176
|
+
|
177
|
+
def self.send_exception_email(ex)
|
178
|
+
email = OpenStruct.new
|
179
|
+
email.recipients = get_recipients
|
180
|
+
email.sender = get_sender
|
181
|
+
email.subject = "An exception occured while checking replication at #{Time.now}"
|
182
|
+
email.body = 'Expception\n\n'
|
183
|
+
email.body += "#{ex.message}\n"
|
184
|
+
ex.backtrace.each do |line|
|
185
|
+
email.body += "#{line}\n"
|
186
|
+
end
|
187
|
+
|
188
|
+
send_email(email)
|
189
|
+
end
|
190
|
+
|
191
|
+
def self.send_email(email)
|
192
|
+
# TODO: could do Net::SMTP.respond_to?(enable_tls) ? enable_TLS : puts "Install TLS gem to use SSL/TLS"
|
193
|
+
Net::SMTP.enable_tls(OpenSSL::SSL::VERIFY_NONE)
|
194
|
+
Net::SMTP.start(configs['email_server'],
|
195
|
+
configs['email_port'],
|
196
|
+
configs['email_domain'],
|
197
|
+
get_sender,
|
198
|
+
configs['email_password'],
|
199
|
+
configs['email_auth_type'].to_sym) do |smtp|
|
200
|
+
email.recipients.each do |email_addy|
|
201
|
+
hdr = "From: #{email.sender}\n"
|
202
|
+
hdr += "To: #{email_addy} <#{email_addy}>\n"
|
203
|
+
hdr += "Subject: #{email.subject}\n\n"
|
204
|
+
msg = hdr + email.body
|
205
|
+
puts "Sending to #{email_addy}"
|
206
|
+
smtp.send_message msg, email.sender, email_addy
|
207
|
+
end
|
208
|
+
end
|
209
|
+
# TODO: could try and recover
|
210
|
+
# rescue Net::SMTPAuthenticationError => e
|
211
|
+
# if e.message =~ /504 5.7.4 Unrecognized authentication type/
|
212
|
+
# puts "Attempting to load necesary files for TLS/SSL authentication"
|
213
|
+
# puts "Make sure openssl and the tlsmail gem are installed"
|
214
|
+
# require 'openssl'
|
215
|
+
# require 'rubygems'
|
216
|
+
# has_tlsmail_gem = require 'tlsmail'
|
217
|
+
# raise "Please install the 'tlsmail' gem" unless has_tlsmail_gem
|
218
|
+
# Net::SMTP.enable_tls(OpenSSL::SSL::VERIFY_NONE)
|
219
|
+
# send_email(email)
|
220
|
+
# end
|
221
|
+
end
|
222
|
+
|
223
|
+
end
|
224
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
module Reptile
|
2
|
+
# The runner class is responsible for running command on each slave.
|
3
|
+
# The commands are run sequentially, no guaranteed order (though probably from yml file).
|
4
|
+
class Runner
|
5
|
+
# Set the user settings for a user that has REPLICATION SLAVE privilidgess
|
6
|
+
def self.user=(replication_user_settings)
|
7
|
+
@repl_user = replication_user_settings
|
8
|
+
end
|
9
|
+
|
10
|
+
# The user settings for a user that has REPLICATION SLAVE privilidgess
|
11
|
+
def self.user
|
12
|
+
raise "You need to specify a replication user!" if @repl_user.nil?
|
13
|
+
@repl_user
|
14
|
+
end
|
15
|
+
|
16
|
+
# Set the databases to run command upon.
|
17
|
+
def self.databases=(databases)
|
18
|
+
@databases = databases
|
19
|
+
end
|
20
|
+
|
21
|
+
# The databases to run commands upon.
|
22
|
+
def self.databases
|
23
|
+
@databases
|
24
|
+
end
|
25
|
+
|
26
|
+
# Set the slaves to run command upon.
|
27
|
+
def self.slaves=(slaves)
|
28
|
+
slaves.each do |name, configs|
|
29
|
+
configs.delete('port')
|
30
|
+
configs.delete('host')
|
31
|
+
# With activeRecord, you have to connect to some DB, even if you are acting on the server...
|
32
|
+
configs['database'] = 'information_schema'
|
33
|
+
# TODO: Delete these somewhere else
|
34
|
+
configs.delete('heartbeat')
|
35
|
+
configs.delete('replication_user')
|
36
|
+
end
|
37
|
+
@slaves = slaves
|
38
|
+
end
|
39
|
+
|
40
|
+
# The slaves to run commands upon.
|
41
|
+
def self.slaves
|
42
|
+
raise "You need to specify the slaves to run against!" if @slaves.nil?
|
43
|
+
@slaves
|
44
|
+
end
|
45
|
+
|
46
|
+
|
47
|
+
# Tries to establish a database connection, and returns that connection.
|
48
|
+
# Dumps configs on error.
|
49
|
+
def self.connect(configs)
|
50
|
+
ActiveRecord::Base.establish_connection(configs)
|
51
|
+
ActiveRecord::Base.connection
|
52
|
+
rescue Exception => e
|
53
|
+
puts "****"
|
54
|
+
puts "Error connecting to database: #{e}"
|
55
|
+
puts "****"
|
56
|
+
puts YAML::dump(configs)
|
57
|
+
exit 1
|
58
|
+
end
|
59
|
+
|
60
|
+
# Executes a command on all the slaves, sequentially.
|
61
|
+
# Takes an optional set of connection paramters to override defaults.
|
62
|
+
def self.execute_on_slaves(cmd, configs={})
|
63
|
+
slaves.each do |name, slave_configs|
|
64
|
+
puts "Executing #{cmd} on #{name}"
|
65
|
+
puts slave_configs.inspect
|
66
|
+
connection = connect(slave_configs.merge(user).merge(configs))
|
67
|
+
connection.execute(cmd)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Execute STOP SLAVE on all slaves;
|
72
|
+
def self.stop_slaves
|
73
|
+
execute_on_slaves("STOP SLAVE;")
|
74
|
+
end
|
75
|
+
|
76
|
+
# Execute START SLAVE on all slaves.
|
77
|
+
def self.start_slaves
|
78
|
+
execute_on_slaves("START SLAVE;")
|
79
|
+
end
|
80
|
+
|
81
|
+
# Creates users with specific permissions on the different mysql servers, both masters and slaves.
|
82
|
+
# Prompts for username and password of an account that has GRANT priviledges.
|
83
|
+
def self.setup
|
84
|
+
raise "GET CONFIGS"
|
85
|
+
grant_user_configs = User.prompt_for_grant_user
|
86
|
+
# TODO: use specific tables, not *.*
|
87
|
+
# TODO: are these on localhost? or where?
|
88
|
+
# TODO: We need all databases in order to grant permissions there.
|
89
|
+
# TODO: There could be a different GRANT password for each mysql server, so identify which before asking for permissions.
|
90
|
+
execute_on_slaves("GRANT select ON *.* TO #{ro_user}@???? INDENTIFIED BY #{ro_password}", grant_user_configs)
|
91
|
+
execute_on_slaves("GRANT select ON *.* TO #{repl_user}@???? INDENTIFIED BY #{repl_password}", grant_user_configs)
|
92
|
+
end
|
93
|
+
|
94
|
+
def self.setup_heartbeat
|
95
|
+
# MySQL DTD for setting up Heartbeats. Change HEARTBEAT_USER, HEARTBEAT_PASS, and MONITORING_BOX to ip of the monitoring server.
|
96
|
+
# GRANT SELECT, INSERT, UPDATE, ALTER ON replication_monitor.* TO 'HEARTBEAT_USER'@"localhost" IDENTIFIED BY 'HEARTBEAT_PASS';
|
97
|
+
# GRANT SELECT, INSERT, UPDATE, ALTER ON replication_monitor.* TO 'HEARTBEAT_USER'@"MONITORING_BOX" IDENTIFIED BY 'HEARTBEAT_PASS';
|
98
|
+
#
|
99
|
+
# CREATE DATABASE replication_monitor;
|
100
|
+
#
|
101
|
+
# CREATE TABLE replication_monitor.heartbeats (
|
102
|
+
# unix_time INTEGER NOT NULL,
|
103
|
+
# db_time TIMESTAMP NOT NULL,
|
104
|
+
# INDEX time_idx(unix_time)
|
105
|
+
# )
|
106
|
+
end
|
107
|
+
|
108
|
+
def self.test_connections
|
109
|
+
databases.each do |db|
|
110
|
+
databases.roles.each do |role|
|
111
|
+
db[role]
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'tlsmail'
|
2
|
+
|
3
|
+
module Reptile
|
4
|
+
# The Status class is responsible for asking a slave database for its status is,
|
5
|
+
# parsing the result, and returning the appropiate status code.
|
6
|
+
#
|
7
|
+
# This class also allows you to convert a status code to a friendly message the corresponds to that status.
|
8
|
+
|
9
|
+
class Status
|
10
|
+
|
11
|
+
@@errors = []
|
12
|
+
|
13
|
+
# Status code indicating the SQL thread has stopped running.
|
14
|
+
SQL_THREAD_DOWN = 'sql_thread_down'
|
15
|
+
|
16
|
+
# Status code indicating the IO thread has stopped running.
|
17
|
+
IO_THREAD_DOWN = 'io_thread_down'
|
18
|
+
|
19
|
+
# Status code indicating that the slave has stopped replicating.
|
20
|
+
SLAVE_DOWN = 'slave_down'
|
21
|
+
|
22
|
+
# Status code indicating that the slave is up and running.
|
23
|
+
RUNNING = 'running'
|
24
|
+
|
25
|
+
# Set the user settings for a user that has global SELECT privilidgess
|
26
|
+
def self.user=(user_settings)
|
27
|
+
@user = user_settings
|
28
|
+
end
|
29
|
+
|
30
|
+
# The user settings for a user that has global select privilidgess
|
31
|
+
def self.user
|
32
|
+
raise "You need to specify a user!" if @user.nil?
|
33
|
+
@user
|
34
|
+
end
|
35
|
+
|
36
|
+
# Checks the value of the MySQL command "SHOW SLAVE STATUS".
|
37
|
+
# Returns a status code.
|
38
|
+
def self.check_slave_status(name, configs)
|
39
|
+
# TODO: Do this in Databases
|
40
|
+
configs.delete("port")
|
41
|
+
configs['database'] = "information_schema"
|
42
|
+
Databases.connect(configs.merge(user)).execute('SHOW SLAVE STATUS').each do |row|
|
43
|
+
if row =~ /Slave_SQL_Running/ && row =~ /No/
|
44
|
+
return SQL_THREAD_DOWN
|
45
|
+
elsif row =~ /Slave_IO_Running/ && row =~ /No/
|
46
|
+
return IO_THREAD_DOWN
|
47
|
+
elsif row =~ /Slave_Running/ && row =~ /No/
|
48
|
+
return SLAVE_DOWN
|
49
|
+
else
|
50
|
+
return RUNNING
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Returns a nice error message for the given status code
|
56
|
+
def self.get_error_message(status)
|
57
|
+
case status
|
58
|
+
when SQL_THREAD_DOWN
|
59
|
+
"The SQL thread has stopped"
|
60
|
+
when IO_THREAD_DOWN
|
61
|
+
"The IO thread has stopped"
|
62
|
+
when SLAVE_DOWN
|
63
|
+
"The slave has stoppped"
|
64
|
+
else
|
65
|
+
raise "Invalid status code. Must be one of #{status_codes.keys.inspect}"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# A hash containing the names of the constants that represent status codes,
|
70
|
+
# and the strings they represent
|
71
|
+
def self.status_codes
|
72
|
+
status_codes = {}
|
73
|
+
self.constants.each do |const|
|
74
|
+
status_codes[const] = const_get(const)
|
75
|
+
end
|
76
|
+
status_codes
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module Reptile
|
2
|
+
# The Users class is holds the different sets of parameters for logging into a database,
|
3
|
+
# including username, password, etc. There are read_only configs, replication user configs,
|
4
|
+
# heartbeat configs, etc, for correctly and safely allowing access to different databases for
|
5
|
+
# different reasons.
|
6
|
+
#
|
7
|
+
# This also data also is used to create the permissions, allowing the setup of replication
|
8
|
+
# to be even easier.
|
9
|
+
class Users
|
10
|
+
def initialize(options)
|
11
|
+
@repl_user = options["replication_user"]
|
12
|
+
@ro_user = options["ro_user"]
|
13
|
+
@heartbeat_user = options["heartbeat_user"]
|
14
|
+
end
|
15
|
+
|
16
|
+
|
17
|
+
def self.prompt_for_grant_user
|
18
|
+
require 'rubygems'
|
19
|
+
require 'highline'
|
20
|
+
|
21
|
+
asker = HighLine.new
|
22
|
+
asker.say("Please enter credentials for a user that has GRANT priviledges.")
|
23
|
+
{:username => asker.ask("Enter your username:"),
|
24
|
+
:password => asker.ask("Enter your password: ") { |q| q.echo = "x" }}
|
25
|
+
end
|
26
|
+
|
27
|
+
# # Set the user settings for a user that has REPLICATION SLAVE privilidgess
|
28
|
+
# def replication_user=(replication_user_settings)
|
29
|
+
# @repl_user = replication_user_settings
|
30
|
+
# end
|
31
|
+
|
32
|
+
# The user settings for a user that has REPLICATION SLAVE privilidgess
|
33
|
+
def replication_user
|
34
|
+
# TODO: only bail on getting a user if it is acutally used
|
35
|
+
#raise "You need to specify a replication user!" if @repl_user.nil?
|
36
|
+
@repl_user
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
# # Set the user settings for a user that has SELECT privilidgess
|
41
|
+
# def ro_user=(ro_user_settings)
|
42
|
+
# @ro_user = ro_user_settings
|
43
|
+
# end
|
44
|
+
|
45
|
+
# The user settings for a user that has SELECT privilidgess
|
46
|
+
def ro_user
|
47
|
+
#raise "You need to specify a SELECT user!" if @ro_user.nil?
|
48
|
+
@ro_user || {}
|
49
|
+
end
|
50
|
+
|
51
|
+
|
52
|
+
# # Set the user settings for a user that has reads/writes heartbeats
|
53
|
+
# def heartbeat_user=(heartbeat_user_settings)
|
54
|
+
# @heartbeat_user = heartbeat_user_settings
|
55
|
+
# end
|
56
|
+
|
57
|
+
# The user settings for a user that reads/writes heartbeats
|
58
|
+
def heartbeat_user
|
59
|
+
#raise "You need to specify a heartbeat user!" if @heartbeat_user.nil?
|
60
|
+
@heartbeat_user || {}
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
end
|
data/lib/reptile.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
$:.unshift(File.dirname(__FILE__)) unless
|
2
|
+
$:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
|
3
|
+
|
4
|
+
require 'ostruct'
|
5
|
+
require 'openssl'
|
6
|
+
require 'rubygems'
|
7
|
+
require 'tlsmail'
|
8
|
+
require 'net/smtp'
|
9
|
+
|
10
|
+
|
11
|
+
require 'reptile/heartbeat'
|
12
|
+
require 'reptile/delta_monitor'
|
13
|
+
require 'reptile/replication_monitor'
|
14
|
+
require 'reptile/status'
|
15
|
+
require 'reptile/runner'
|
16
|
+
require 'reptile/users'
|
17
|
+
require 'reptile/databases'
|
18
|
+
|
19
|
+
require 'active_record'
|
20
|
+
|
21
|
+
module Reptile
|
22
|
+
VERSION = '0.0.1'
|
23
|
+
end
|
data/script/console
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# File: script/console
|
3
|
+
irb = RUBY_PLATFORM =~ /(:?mswin|mingw)/ ? 'irb.bat' : 'irb'
|
4
|
+
|
5
|
+
libs = " -r irb/completion"
|
6
|
+
# Perhaps use a console_lib to store any extra methods I may want available in the cosole
|
7
|
+
# libs << " -r #{File.dirname(__FILE__) + '/../lib/console_lib/console_logger.rb'}"
|
8
|
+
libs << " -r #{File.dirname(__FILE__) + '/../lib/reptile.rb'}"
|
9
|
+
puts "Loading reptile gem"
|
10
|
+
exec "#{irb} #{libs} --simple-prompt"
|
data/script/destroy
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
APP_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..'))
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'rubigen'
|
6
|
+
rescue LoadError
|
7
|
+
require 'rubygems'
|
8
|
+
require 'rubigen'
|
9
|
+
end
|
10
|
+
require 'rubigen/scripts/destroy'
|
11
|
+
|
12
|
+
ARGV.shift if ['--help', '-h'].include?(ARGV[0])
|
13
|
+
RubiGen::Base.use_component_sources! [:rubygems, :newgem, :newgem_theme, :test_unit]
|
14
|
+
RubiGen::Scripts::Destroy.new.run(ARGV)
|
data/script/generate
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
APP_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..'))
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'rubigen'
|
6
|
+
rescue LoadError
|
7
|
+
require 'rubygems'
|
8
|
+
require 'rubigen'
|
9
|
+
end
|
10
|
+
require 'rubigen/scripts/generate'
|
11
|
+
|
12
|
+
ARGV.shift if ['--help', '-h'].include?(ARGV[0])
|
13
|
+
RubiGen::Base.use_component_sources! [:rubygems, :newgem, :newgem_theme, :test_unit]
|
14
|
+
RubiGen::Scripts::Generate.new.run(ARGV)
|
data/test/test_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: reptile
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- FIXME full name
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-06-26 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: newgem
|
17
|
+
type: :development
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 1.4.1
|
24
|
+
version:
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: hoe
|
27
|
+
type: :development
|
28
|
+
version_requirement:
|
29
|
+
version_requirements: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 1.8.0
|
34
|
+
version:
|
35
|
+
description: ""
|
36
|
+
email:
|
37
|
+
- FIXME email
|
38
|
+
executables:
|
39
|
+
- replication_status
|
40
|
+
extensions: []
|
41
|
+
|
42
|
+
extra_rdoc_files:
|
43
|
+
- History.txt
|
44
|
+
- Manifest.txt
|
45
|
+
- PostInstall.txt
|
46
|
+
- README.rdoc
|
47
|
+
files:
|
48
|
+
- History.txt
|
49
|
+
- Manifest.txt
|
50
|
+
- PostInstall.txt
|
51
|
+
- README.rdoc
|
52
|
+
- Rakefile
|
53
|
+
- bin/replication_status
|
54
|
+
- lib/reptile.rb
|
55
|
+
- lib/reptile/databases.rb
|
56
|
+
- lib/reptile/delta_monitor.rb
|
57
|
+
- lib/reptile/dtd.sql
|
58
|
+
- lib/reptile/heartbeat.rb
|
59
|
+
- lib/reptile/replication_monitor.rb
|
60
|
+
- lib/reptile/runner.rb
|
61
|
+
- lib/reptile/status.rb
|
62
|
+
- lib/reptile/users.rb
|
63
|
+
- script/console
|
64
|
+
- script/destroy
|
65
|
+
- script/generate
|
66
|
+
- test/test_helper.rb
|
67
|
+
- test/test_reptile.rb
|
68
|
+
has_rdoc: true
|
69
|
+
homepage: Reptile is an easy to use utility that will monitor your MySQL replication, so you can forget about it and focus on the good stuff. It provides a utility for generate replication reports, and can email if replication appears to be failing.
|
70
|
+
post_install_message: PostInstall.txt
|
71
|
+
rdoc_options:
|
72
|
+
- --main
|
73
|
+
- README.rdoc
|
74
|
+
require_paths:
|
75
|
+
- lib
|
76
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
77
|
+
requirements:
|
78
|
+
- - ">="
|
79
|
+
- !ruby/object:Gem::Version
|
80
|
+
version: "0"
|
81
|
+
version:
|
82
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
83
|
+
requirements:
|
84
|
+
- - ">="
|
85
|
+
- !ruby/object:Gem::Version
|
86
|
+
version: "0"
|
87
|
+
version:
|
88
|
+
requirements: []
|
89
|
+
|
90
|
+
rubyforge_project: reptile
|
91
|
+
rubygems_version: 1.3.1
|
92
|
+
signing_key:
|
93
|
+
specification_version: 2
|
94
|
+
summary: ""
|
95
|
+
test_files:
|
96
|
+
- test/test_helper.rb
|
97
|
+
- test/test_reptile.rb
|