reptile 0.0.6 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source :gemcutter
2
+
3
+ # Specify your gem's dependencies in reptile.gemspec
4
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,37 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ reptile (0.1.0)
5
+ activerecord
6
+ mixlib-log (>= 1.2.0)
7
+ tlsmail (>= 0.0.1)
8
+
9
+ GEM
10
+ remote: http://rubygems.org/
11
+ specs:
12
+ activemodel (3.0.4)
13
+ activesupport (= 3.0.4)
14
+ builder (~> 2.1.2)
15
+ i18n (~> 0.4)
16
+ activerecord (3.0.4)
17
+ activemodel (= 3.0.4)
18
+ activesupport (= 3.0.4)
19
+ arel (~> 2.0.2)
20
+ tzinfo (~> 0.3.23)
21
+ activesupport (3.0.4)
22
+ arel (2.0.8)
23
+ builder (2.1.2)
24
+ i18n (0.5.0)
25
+ mixlib-log (1.2.0)
26
+ tlsmail (0.0.1)
27
+ tzinfo (0.3.24)
28
+
29
+ PLATFORMS
30
+ ruby
31
+
32
+ DEPENDENCIES
33
+ activerecord
34
+ bundler (>= 1.0.0)
35
+ mixlib-log (>= 1.2.0)
36
+ reptile!
37
+ tlsmail (>= 0.0.1)
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ (The MIT License)
2
+
3
+ Copyright (c) 2010 Nick Stielau
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ 'Software'), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc CHANGED
@@ -1,6 +1,12 @@
1
1
  = reptile
2
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.
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.
4
+
5
+ == STATUS DISCLAIMER:
6
+
7
+ I'm not sure if anybody uses this. I PROBABLY WON'T fix a patch you submit. This, as it is, has been used successfully in production environments. It's a good idea, but might not work for your particular case, and I make no guarantee that it will. I certainly hope it does. Drop me a line, and I'll answer any questions you have. NCS 8/2010
8
+
9
+ The --diff check can cause problems on large tables, as it does a lot of SELECT COUNT(*) FROM..'s .
4
10
 
5
11
  == REQUIREMENTS:
6
12
 
@@ -14,10 +20,14 @@ Put a config file at /etc/reptile.yml, /etc/reptile/reptile.yml, ./reptile.yml,
14
20
  [nick@s34 ~]$ replication_status -h
15
21
  Usage: replication_status [path_to_config_file]
16
22
  -h, --help Displays this help info
17
- -s, --status Displays the slave status
18
- -d, --diff Checks the row count difference between master and slave
19
- -r, --report Sends a report email
20
- -b, --heartbeat Checks the heartbeat timestamp difference between master and slave
23
+ --status Displays the slave status
24
+ --diff Checks the row count difference between master and slave
25
+ --report Prints a report
26
+ --heartbeat Checks the heartbeat timestamp difference between master and slave
27
+ --stop-slaves Stops all slaves
28
+ --start-slaves Starts all slaves
29
+ -l, --log-level [LEVEL] Specify log level (debug,info,warn,error,fatal)
30
+
21
31
  [nick@s34 ~]$ replication_status -s
22
32
  * a_database slave is up and running
23
33
  * b_database slave is up and running
@@ -28,12 +38,14 @@ Put a config file at /etc/reptile.yml, /etc/reptile/reptile.yml, ./reptile.yml,
28
38
  [nick@s34 ~]$ sudo crontab -l
29
39
  Password:
30
40
  MAILTO="user@example.com"
31
- # Use the '-e' arg to only spit out errors, which will be sent out by crontab
32
- */15 * * * * replication_status -e
41
+ # Use the '-l error' arg to only spit out errors, which will be mailed by crontab
42
+ */15 * * * * replication_status -l error
43
+ # Or, specify certain checks
44
+ # */15 * * * * replication_status -l error --status
33
45
 
34
46
  === Setup SQL
35
47
 
36
- Run this SQL to setup the DBs/perms for Reptile. Using different users, since each user requires significantly different permissions.
48
+ Run this SQL to setup the DBs/perms for Reptile. Using different users, since each user requires significantly different permissions.
37
49
 
38
50
  GRANT REPLICATION SLAVE, REPLICATION CLIENT, SUPER ON *.* TO 'repl'@"localhost" IDENTIFIED BY 'repl_user_pass';
39
51
  GRANT REPLICATION SLAVE, REPLICATION CLIENT, SUPER ON *.* TO 'repl'@"MONITOR_HOST_IP" IDENTIFIED BY 'repl_user_pass';
@@ -47,36 +59,11 @@ Run this SQL to setup the DBs/perms for Reptile. Using different users, since e
47
59
  GRANT SELECT, INSERT, UPDATE, ALTER ON replication_monitor.* TO 'heartbeat_user'@"MONITOR_HOST_IP" IDENTIFIED BY 'heartbeat_user_pass';
48
60
 
49
61
  CREATE TABLE replication_monitor.heartbeats (
50
- unix_time INTEGER NOT NULL,
51
- db_time TIMESTAMP NOT NULL,
62
+ unix_time INTEGER NOT NULL,
63
+ db_time TIMESTAMP NOT NULL,
52
64
  INDEX time_idx(unix_time)
53
65
  )
54
66
 
55
67
  == INSTALL:
56
68
 
57
69
  sudo gem install reptile
58
-
59
- == LICENSE:
60
-
61
- (The MIT License)
62
-
63
- Copyright (c) 2009 Nick Stielau
64
-
65
- Permission is hereby granted, free of charge, to any person obtaining
66
- a copy of this software and associated documentation files (the
67
- 'Software'), to deal in the Software without restriction, including
68
- without limitation the rights to use, copy, modify, merge, publish,
69
- distribute, sublicense, and/or sell copies of the Software, and to
70
- permit persons to whom the Software is furnished to do so, subject to
71
- the following conditions:
72
-
73
- The above copyright notice and this permission notice shall be
74
- included in all copies or substantial portions of the Software.
75
-
76
- THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
77
- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
78
- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
79
- IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
80
- CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
81
- TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
82
- SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile CHANGED
@@ -1,23 +1,11 @@
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'
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
4
3
 
5
- begin
6
- require 'jeweler'
7
- Jeweler::Tasks.new do |gem|
8
- gem.name = "reptile"
9
- gem.summary = %Q{Cold blooded mysql replication monitoring.}
10
- gem.description = %Q{Cold blooded mysql replication monitoring.}
11
- gem.email = "nick.stielau@gmail.com"
12
- gem.homepage = "http://reptile.rubyforge.org/"
13
- gem.authors = ["Nick Stielau"]
14
- gem.add_runtime_dependency 'tlsmail', '>= 0'
15
- gem.add_runtime_dependency 'activerecord', '>= 0'
16
- # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
17
- end
18
- Jeweler::GemcutterTasks.new
19
- rescue LoadError
20
- puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
4
+ require 'rake/testtask'
5
+ Rake::TestTask.new(:test) do |test|
6
+ test.libs << 'lib' << 'test'
7
+ test.pattern = 'test/**/test_*.rb'
8
+ test.verbose = true
21
9
  end
22
10
 
23
11
  desc "Upload current documentation to Rubyforge"
@@ -31,8 +19,5 @@ task :upload_site do
31
19
  sh "scp -r webgen_site/* nstielau@rubyforge.org:/var/www/gforge-projects/reptile/"
32
20
  end
33
21
 
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]
22
+ task :test => :check_dependencies
23
+ task :default => :test
@@ -1,9 +1,13 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require 'rubygems'
4
3
  require 'optparse'
4
+
5
+ require 'rubygems'
5
6
  require 'reptile'
6
7
 
8
+ Reptile::Log.level = :info
9
+ Mixlib::Log::Formatter.show_time = false
10
+
7
11
  commands = []
8
12
 
9
13
  OptionParser.new do |opts|
@@ -13,33 +17,35 @@ OptionParser.new do |opts|
13
17
  puts opts
14
18
  exit 0
15
19
  end
16
-
17
- opts.on("-s", "--status", "Displays the slave status") do
20
+
21
+ opts.on("--status", "Displays the slave status") do
18
22
  commands << 'check_slaves'
19
23
  end
20
-
21
- opts.on("-d", "--diff", "Checks the row count difference between master and slave") do
24
+
25
+ opts.on("--diff", "Checks the row count difference between master and slave") do
22
26
  commands << 'diff_tables'
23
27
  end
24
-
25
- opts.on("-r", "--report", "Sends a report email") do
28
+
29
+ opts.on("--report", "Prints a report") do
26
30
  commands << 'report'
27
31
  end
28
-
29
- opts.on("-b", "--heartbeat", "Checks the heartbeat timestamp difference between master and slave") do
32
+
33
+ opts.on("--heartbeat", "Checks the heartbeat timestamp difference between master and slave") do
30
34
  commands << 'heartbeat'
31
35
  end
32
-
33
- opts.on("-x", "--stop_slaves", "Stops all slaves") do
36
+
37
+ opts.on("--stop-slaves", "Stops all slaves") do
34
38
  commands << 'stop_slaves'
35
39
  end
36
-
37
- opts.on("-g", "--start_slaves", "Starts all slaves") do
40
+
41
+ opts.on("--start-slaves", "Starts all slaves") do
38
42
  commands << 'start_slaves'
39
43
  end
40
-
41
- opts.on("-e", "--errors", "Show only errors.") do
42
- commands << 'errors'
44
+
45
+ log_levels = [:debug, :info, :warn, :error, :fatal]
46
+ opts.on("-l", "--log-level [LEVEL]", log_levels,
47
+ "Specify log level (#{log_levels.join(',')})") do |t|
48
+ Reptile::Log.level = t.to_sym
43
49
  end
44
50
 
45
51
  begin
@@ -52,24 +58,20 @@ OptionParser.new do |opts|
52
58
  end
53
59
 
54
60
  if !ARGV.empty? && ARGV.length > 1
55
- abort "Too many arguments; please specify only the directory containing the 'reptile.yml' file, or a yml config file itself."
61
+ abort "Too many arguments; please specify only the config file."
56
62
  end
57
63
 
58
- config_file_name = "reptile.yml"
59
- config_file_locations = ["/etc/#{config_file_name}", "/etc/reptile/#{config_file_name}", "./#{config_file_name}"]
60
64
  config_location_param = ARGV.first
61
65
 
62
66
  config_file = nil
63
67
  if config_location_param
64
- if File.directory?(config_location_param) && File.exist?(File.join(config_location_param, config_file_name))
65
- config_file = File.join(config_location_param, config_file_name)
66
- elsif File.exist?(config_location_param) && !File.directory?(config_location_param)
68
+ if File.exist?(config_location_param) && !File.directory?(config_location_param)
67
69
  config_file = config_location_param
68
70
  else
69
- abort "Please specify the directory containing the '#{config_file_name}' file, or the config file itself."
71
+ abort "Please specify the config file."
70
72
  end
71
73
  else
72
- config_file_locations.each do |f|
74
+ ["/etc/reptile.yml", "/etc/reptile/reptile.yml", "./reptile.yml"].each do |f|
73
75
  if File.exist?(f)
74
76
  config_file = f
75
77
  break
@@ -80,11 +82,11 @@ else
80
82
  end
81
83
  end
82
84
 
83
-
85
+ Reptile::Log.debug "Loading config from #{config_file}"
84
86
  Reptile::ReplicationMonitor.load_config_file(config_file)
85
87
 
86
88
  if (commands.include?('start_slaves') || commands.include?('stop_slaves'))
87
- Reptile::Runner.send(commands[0])
89
+ Reptile::Runner.send(commands[0])
88
90
  else
89
91
  (commands.empty? ? ['check_slaves', 'heartbeat', 'diff_tables'] : commands).each do |command|
90
92
  Reptile::ReplicationMonitor.send(command)
@@ -3,11 +3,11 @@ module Reptile
3
3
  # for the master and slave of each particular database.
4
4
  class Databases
5
5
  attr :databases
6
-
6
+
7
7
  def initialize(databases)
8
8
  @databases = databases
9
9
  end
10
-
10
+
11
11
  # returns an array of the master names
12
12
  def masters
13
13
  @master_configs ||= get_masters
@@ -17,7 +17,7 @@ module Reptile
17
17
  def slaves
18
18
  @slave_configs ||= get_slaves
19
19
  end
20
-
20
+
21
21
  private
22
22
 
23
23
  def get_masters
@@ -26,7 +26,7 @@ private
26
26
  masters.each_key{|name| masters[name] = masters[name]['master'] }
27
27
  masters
28
28
  end
29
-
29
+
30
30
  # TODO: make private
31
31
  def get_slaves
32
32
  dbs = databases.dup
@@ -34,17 +34,15 @@ private
34
34
  dbs.each_key{|name| dbs[name] = dbs[name]['slave'] }
35
35
  slaves = dbs
36
36
  end
37
-
38
- # Tries to establish a database connection, and returns that connection.
37
+
38
+ # Tries to establish a database connection, and returns that connection.
39
39
  # Dumps configs on error
40
40
  def self.connect(configs)
41
41
  ActiveRecord::Base.establish_connection(configs)
42
42
  ActiveRecord::Base.connection
43
43
  rescue Exception => e
44
- puts "****"
45
- puts "Error connecting to database: #{e}"
46
- puts "****"
47
- puts configs.inspect
44
+ Log.error "Error connecting to database: #{e}"
45
+ Log.error configs.inspect
48
46
  exit 1
49
47
  end
50
48
  end
@@ -1,7 +1,5 @@
1
- require 'logger'
2
-
3
1
  module Reptile
4
- # This monitor compares the row counts for each table for each master and slave.
2
+ # This monitor compares the row counts for each table for each master and slave.
5
3
  class DeltaMonitor
6
4
  # Set the user settings for a user that has global SELECT privilidgess
7
5
  def self.user=(user_settings)
@@ -13,20 +11,12 @@ module Reptile
13
11
  raise "You need to specify a user!" if @user.nil?
14
12
  @user
15
13
  end
16
-
17
- def self.logger=(new_logger)
18
- @logger = new_logger
19
- end
20
-
21
- def self.get_logger
22
- @logger ||= Logger.new(STDOUT)
23
- end
24
-
14
+
25
15
  # Retrieve the active database connection. Nil of none exists.
26
16
  def self.connection
27
17
  ActiveRecord::Base.connection
28
18
  end
29
-
19
+
30
20
  # Compares the row counts for master tables and slave tables
31
21
  # Returns a hash of TABLE_NAME => ROW COUNT DELTAs
32
22
  def self.diff(db_name, master_configs, slave_configs)
@@ -37,36 +27,36 @@ module Reptile
37
27
  master_counts = get_table_counts
38
28
 
39
29
  deltas= {}
40
- master_counts.each do |table, master_count|
30
+ master_counts.each do |table, master_count|
41
31
  if slave_counts[table].nil?
42
- puts "Table '#{table}' exists on master but not on slave."
32
+ Log.error "Table '#{table}' exists on master but not on slave."
43
33
  next
44
- end
34
+ end
45
35
  delta = master_count.first.to_i - slave_counts[table].first.to_i
46
36
  deltas[table] = delta
47
37
  end
48
-
38
+
49
39
  print_deltas(db_name, deltas, master_configs)
50
-
40
+
51
41
  deltas
52
42
  rescue Exception => e
53
- get_logger.error "Error: Caught #{e}"
54
- get_logger.error "DB Name: #{db_name}"
55
- get_logger.error "Master Configs: #{master_configs.inspect}"
56
- get_logger.error "Slave Configs: #{slave_configs.inspect}"
43
+ Log.error "Error: Caught #{e}"
44
+ Log.error "DB Name: #{db_name}"
45
+ Log.error "Master Configs: #{master_configs.inspect}"
46
+ Log.error "Slave Configs: #{slave_configs.inspect}"
57
47
  raise e
58
48
  end
59
-
49
+
60
50
  # Prints stats about the differences in number of rows between the master and slave
61
51
  def self.print_deltas(db_name, deltas, configs)
62
52
  non_zero_deltas = deltas.select{|table, delta| not delta.zero?}
63
53
  if non_zero_deltas.size.zero?
64
- get_logger.info "Replication counts A-OK for #{db_name} on #{configs['host']} @ #{Time.now}"
54
+ Log.info "Replication counts A-OK for #{db_name} on #{configs['host']} @ #{Time.now}"
65
55
  else
66
- get_logger.info "Replication Row Count Deltas for #{db_name} on #{configs['host']} @ #{Time.now}"
67
- get_logger.info "There #{non_zero_deltas.size > 1 ? 'are' : 'is'} #{non_zero_deltas.size} #{non_zero_deltas.size > 1 ? 'deltas' : 'delta'}"
56
+ Log.info "Replication Row Count Deltas for #{db_name} on #{configs['host']} @ #{Time.now}"
57
+ Log.info "There #{non_zero_deltas.size > 1 ? 'are' : 'is'} #{non_zero_deltas.size} #{non_zero_deltas.size > 1 ? 'deltas' : 'delta'}"
68
58
  non_zero_deltas.each do |table, delta|
69
- get_logger.info " #{table} table: #{delta}" unless delta.zero?
59
+ Log.info " #{table} table: #{delta}" unless delta.zero?
70
60
  end
71
61
  end
72
62
  end
@@ -78,7 +68,7 @@ module Reptile
78
68
  connection.execute('SHOW TABLES').each { |row| tables << row }
79
69
  tables
80
70
  end
81
-
71
+
82
72
  # Returns a hash of TABLE_NAME => # Rows for all tables in current db
83
73
  def self.get_table_counts
84
74
  tables = get_tables
@@ -1,18 +1,18 @@
1
1
  require 'active_record'
2
2
 
3
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.
4
+ # MySQL DTD for setting up Heartbeats. Change HEARTBEAT_USER, HEARTBEAT_PASS, and MONITORING_BOX
5
+ # to ip of the monitoring server.
6
6
  #
7
- # GRANT SELECT, INSERT, UPDATE, ALTER ON replication_monitor.*
7
+ # GRANT SELECT, INSERT, UPDATE, ALTER ON replication_monitor.*
8
8
  # TO 'HEARTBEAT_USER'@"localhost" IDENTIFIED BY 'HEARTBEAT_PASS'; GRANT SELECT, INSERT, UPDATE,
9
9
  # ALTER ON replication_monitor.* TO 'HEARTBEAT_USER'@"MONITORING_BOX" IDENTIFIED BY 'HEARTBEAT_PASS';
10
- #
10
+ #
11
11
  # CREATE DATABASE replication_monitor;
12
- #
12
+ #
13
13
  # CREATE TABLE replication_monitor.heartbeats (
14
- # unix_time INTEGER NOT NULL,
15
- # db_time TIMESTAMP NOT NULL,
14
+ # unix_time INTEGER NOT NULL,
15
+ # db_time TIMESTAMP NOT NULL,
16
16
  # INDEX time_idx(unix_time)
17
17
  # )
18
18
  #
@@ -23,59 +23,52 @@ module Reptile
23
23
  def self.user=(default_configs)
24
24
  @user = default_configs
25
25
  end
26
-
26
+
27
27
  # The default connection settings which override per-database settings.
28
28
  def self.user
29
29
  @user ||= {}
30
30
  end
31
-
31
+
32
32
  def self.connect(configs)
33
33
  Databases.connect(configs.merge(user).merge("database" => 'replication_monitor'))
34
34
  end
35
-
35
+
36
36
  # Write a heartbeat.
37
37
  # Thump thump.
38
38
  def self.write(name, configs)
39
39
  self.connect(configs)
40
40
  heartbeat = Heartbeat.create(:unix_time => Time.now.to_i, :db_time => "NOW()")
41
- get_logger.info "Wrote heartbeat to #{name} at #{Time.at(heartbeat.unix_time)}"
41
+ Log.debug "Wrote heartbeat to #{name} at #{Time.at(heartbeat.unix_time)}"
42
42
  end
43
43
 
44
44
  # Read the most recent heartbeat and return the delay in seconds, or nil if no heartbeat are found.
45
45
  def self.read(name, configs)
46
46
  self.connect(configs)
47
-
47
+
48
48
  current_time = Time.now
49
49
 
50
50
  delay = nil
51
51
  heartbeat = Heartbeat.find(:first, :order => 'db_time DESC')
52
-
52
+
53
53
  # No heartbeats at all!
54
54
  if heartbeat.nil?
55
- get_logger.info "No heartbeats found on #{name} at #{Time.now}"
55
+ Log.info "No heartbeats found on #{name} at #{Time.now}"
56
56
  return nil;
57
57
  end
58
-
58
+
59
59
  # Not sure why we have both, (one is easier to read?).
60
60
  # Use one or the other to calculate delay...
61
61
  delay = (Time.now - Time.at(heartbeat.unix_time)).round
62
62
  #delay = (Time.now - heartbeat.db_time)
63
63
 
64
- get_logger.info "Read heartbeat from #{name} at #{Time.at(heartbeat.unix_time)}. The delay is #{strfdelay(delay)}"
65
-
64
+ Log.debug "Read heartbeat from #{name} at #{Time.at(heartbeat.unix_time)}."
65
+ Log.info "The delay is #{strfdelay(delay)}"
66
+
66
67
  delay
67
68
  end
68
-
69
- def self.logger=(new_logger)
70
- @logger = new_logger
71
- end
72
-
73
- def self.get_logger
74
- @logger ||= Logger.new(STDOUT)
75
- end
76
-
69
+
77
70
  private
78
-
71
+
79
72
  # Format the delay (in seconds) as a human-readable string.
80
73
  def self.strfdelay(delay)
81
74
  seconds = delay % 60
@@ -0,0 +1,6 @@
1
+ require 'mixlib/log'
2
+ module Reptile
3
+ class Log
4
+ extend Mixlib::Log
5
+ end
6
+ end
@@ -1,166 +1,130 @@
1
- require 'logger'
2
-
3
1
  module Reptile
4
2
  class ReplicationMonitor
5
-
3
+
6
4
  # Attempts to load the replication.yml configuration file.
7
- def self.load_config_file(databases_file)
5
+ def self.load_config_file(databases_file)
8
6
  @databases_file = databases_file
9
7
  yaml = YAML::load(File.read(@databases_file))
10
8
  @configs = yaml.delete('config')
11
9
  @users = Users.new(yaml.delete('users'))
12
10
  @databases = Databases.new(yaml)
13
-
11
+
14
12
  Heartbeat.user = users.heartbeat_user
15
13
  Runner.user = users.replication_user
16
14
  Status.user = users.replication_user
17
15
  DeltaMonitor.user = users.ro_user
18
16
  Runner.databases = databases
19
-
17
+
20
18
  raise "Please specify a delay threshold 'delay_threshold_secs: 360'" if @configs['delay_threshold_secs'].nil?
21
19
  raise "Please specify a row delta threshold 'row_difference_threshold: 10'" if @configs['row_difference_threshold'].nil?
22
-
23
20
  rescue Errno::EACCES => e
24
- puts "Unable to open config file: Permission Denied"
21
+ Log.error "Unable to open config file: Permission Denied"
25
22
  end
26
-
23
+
27
24
  # Returns the configs from the replication.yml file
28
25
  def self.configs
29
26
  @configs
30
27
  end
31
-
28
+
32
29
  # Returns the databases from the yml file.
33
30
  def self.databases
34
31
  @databases
35
32
  end
36
-
33
+
37
34
  # Returns the +Users+ loaded from the replication.yml file
38
35
  def self.users
39
36
  @users
40
37
  end
41
-
42
- def self.errors
43
- get_logger.sev_threshold = Logger::ERROR
44
- check_slaves
45
- heartbeat
46
- diff_tables
47
- end
48
-
38
+
49
39
  def self.diff_tables
40
+ Log.info "Checking row counts."
50
41
  unsynced_dbs = 0
51
- DeltaMonitor.logger = get_logger
52
-
42
+
53
43
  databases.databases.each_pair do |name, roles|
54
44
  master, slave = roles['master'], roles['slave']
55
45
  deltas = DeltaMonitor.diff(name, master, slave)
56
-
57
- egregious_deltas = deltas.select{|table, delta| delta > configs['row_difference_threshold'] }
46
+
47
+ egregious_deltas = deltas.select{|table, delta| delta > configs['row_difference_threshold'] }
58
48
  if egregious_deltas.size > 0
59
- queue_replication_warning :host => master["host"], :database => master["database"], :deltas => egregious_deltas, :noticed_at => Time.now
49
+ log_replication_error :host => master["host"], :database => master["database"], :deltas => egregious_deltas, :noticed_at => Time.now
60
50
  unsynced_dbs += 1
61
51
  end
62
52
  end
63
-
53
+
64
54
  unsynced_dbs.zero?
65
55
  end
66
-
56
+
67
57
  def self.heartbeat
68
- Heartbeat.logger = get_logger
69
-
58
+ Log.info "Checking heartbeats."
70
59
  databases.masters.each_pair do |name, configs|
71
60
  Heartbeat.write(name, configs)
72
61
  end
73
-
62
+
74
63
  overdue_slaves = 0
75
-
64
+
76
65
  databases.slaves.each_pair do |name, db_configs|
77
66
  delay = Heartbeat.read(name, db_configs)
78
67
  if delay.nil?
79
- queue_replication_warning :host => name,
80
- :database => configs[:database],
81
- :general_error => "Error: No Heartbeats found.",
68
+ log_replication_error :host => name,
69
+ :database => configs[:database],
70
+ :general_error => "Error: No Heartbeats found.",
82
71
  :noticed_at => Time.now
83
72
  overdue_slaves += 1
84
73
  elsif delay > configs['delay_threshold_secs']
85
- queue_replication_warning :host => name,
86
- :database => configs[:database],
87
- :delay => Heartbeat.strfdelay(delay),
74
+ log_replication_error :host => name,
75
+ :database => configs[:database],
76
+ :delay => Heartbeat.strfdelay(delay),
88
77
  :noticed_at => Time.now
89
78
  overdue_slaves += 1
90
79
  end
91
80
  end
92
-
81
+
93
82
  overdue_slaves.zero?
94
83
  end
95
-
84
+
96
85
  # Checks the status of each slave.
97
86
  def self.check_slaves
87
+ Log.info "Checking slave status."
98
88
  databases.slaves.each do |slave_name, slave_configs|
99
89
  status = Status.check_slave_status(slave_name, slave_configs)
100
- get_logger.info "'#{slave_name}' is '#{status}'"
90
+ Log.info "'#{slave_name}' is '#{status}'"
101
91
  if status != Status.const_get(:RUNNING)
102
- queue_replication_warning :host => slave_name,
103
- :database => configs[:database],
104
- :status_error => Status.get_error_message(status),
92
+ log_replication_error :host => slave_name,
93
+ :database => configs[:database],
94
+ :status_error => Status.get_error_message(status),
105
95
  :noticed_at => Time.now
106
96
  end
107
97
  end
108
98
  end
109
-
110
- def self.queue_replication_warning(options)
111
- email = OpenStruct.new
112
- email.recipients = get_recipients
113
- email.subject = "A replication error occured on #{options[:host]} at #{Time.now}"
114
- email.body = ''
115
-
99
+
100
+ def self.log_replication_error(options)
101
+ Log.error = "A replication error occured on #{options[:host]} at #{Time.now}"
102
+
116
103
  if options[:delay]
117
- email.body += "There was a #{options[:delay]} second replication latency, which is greater than the allowed latency of #{configs['delay_threshold_secs']} seconds"
104
+ Log.error "There was a #{options[:delay]} second replication latency, which is greater than the allowed latency of #{configs['delay_threshold_secs']} seconds"
118
105
  elsif options[:deltas]
119
- email.body += "The following tables have master/slave row count difference greater than the allowed #{configs['row_difference_threshold']}\n\n"
120
- options[:deltas].each do |table, delta|
121
- email.body += " table '#{table}' was off by #{delta} rows\n"
106
+ Log.error "The following tables have master/slave row count difference greater than the allowed #{configs['row_difference_threshold']}"
107
+ options[:deltas].each do |table, delta|
108
+ Log.error " table '#{table}' was off by #{delta} rows"
122
109
  end
123
110
  elsif options[:status_error]
124
- email.body += " MySQL Status message: #{options[:status_error]}"
111
+ Log.error " MySQL Status message: #{options[:status_error]}"
125
112
  elsif options[:general_error]
126
- email.body += " Error: #{options[:general_error]}"
113
+ Log.error " Error: #{options[:general_error]}"
127
114
  end
128
-
129
- email.body += "\n"
130
- email.body += " Server: #{options[:host]}\n"
131
- email.body += " Database: #{options[:database]}\n" unless options[:database].blank?
132
-
133
- # Print out email body to STDOUT
134
- get_logger.error email.body
135
-
136
- send_email(email)
137
- end
138
-
139
- # Gets the 'email_to' value from the 'configs' section of the replication.yml file
140
- def self.get_recipients
141
- configs['email_to']
142
- end
143
-
144
- # Gets the 'email_from' value from the 'configs' section of the replication.yml file
145
- def self.get_sender
146
- configs['email_from']
115
+
116
+ Log.error " Server: #{options[:host]}\n"
117
+ Log.error " Database: #{options[:database]}\n" unless options[:database].blank?
147
118
  end
148
-
119
+
149
120
  def self.report
150
- email = OpenStruct.new
151
- email.recipients = get_recipients
152
- email.sender = get_sender
153
- raise "Please specify report recipients 'email_to: user@address.com'" if email.recipients.nil?
154
- raise "Please specify report recipients 'email_from: user@address.com'" if email.sender.nil?
155
-
156
- email.subject = "Daily Replication Report for #{Time.now.strftime('%D')}"
157
-
158
- puts "Generating report email"
159
-
121
+ Log.info "Generating report"
122
+
160
123
  old_stdout = $stdout
161
124
  out = StringIO.new
162
125
  $stdout = out
163
126
  begin
127
+ puts "Daily Replication Report for #{Time.now.strftime('%D')}"
164
128
  puts " Checking slave status"
165
129
  puts
166
130
  self.check_slaves
@@ -181,69 +145,7 @@ module Reptile
181
145
  ensure
182
146
  $stdout = old_stdout
183
147
  end
184
- email.body = out.string
185
-
186
- puts "Sending report email"
187
-
188
- send_email(email)
189
-
190
- puts "Report sent to #{get_recipients}"
148
+ puts out.string
191
149
  end
192
-
193
- def self.send_exception_email(ex)
194
- email = OpenStruct.new
195
- email.recipients = get_recipients
196
- email.sender = get_sender
197
- email.subject = "An exception occured while checking replication at #{Time.now}"
198
- email.body = 'Expception\n\n'
199
- email.body += "#{ex.message}\n"
200
- ex.backtrace.each do |line|
201
- email.body += "#{line}\n"
202
- end
203
-
204
- send_email(email)
205
- end
206
-
207
- def self.send_email(email)
208
- return unless configs['email_server'] && configs['email_port'] && configs['email_domain'] &&
209
- configs['email_password'] && configs['email_auth_type']
210
-
211
- # TODO: could do Net::SMTP.respond_to?(enable_tls) ? enable_TLS : puts "Install TLS gem to use SSL/TLS"
212
- Net::SMTP.enable_tls(OpenSSL::SSL::VERIFY_NONE)
213
- Net::SMTP.start(configs['email_server'],
214
- configs['email_port'],
215
- configs['email_domain'],
216
- get_sender,
217
- configs['email_password'],
218
- configs['email_auth_type'].to_sym) do |smtp|
219
- email.recipients.each do |email_addy|
220
- hdr = "From: #{email.sender}\n"
221
- hdr += "To: #{email_addy} <#{email_addy}>\n"
222
- hdr += "Subject: #{email.subject}\n\n"
223
- msg = hdr + email.body
224
- puts "Sending to #{email_addy}"
225
- smtp.send_message msg, email.sender, email_addy
226
- end
227
- end
228
- # TODO: could try and recover
229
- # rescue Net::SMTPAuthenticationError => e
230
- # if e.message =~ /504 5.7.4 Unrecognized authentication type/
231
- # puts "Attempting to load necesary files for TLS/SSL authentication"
232
- # puts "Make sure openssl and the tlsmail gem are installed"
233
- # require 'openssl'
234
- # require 'rubygems'
235
- # has_tlsmail_gem = require 'tlsmail'
236
- # raise "Please install the 'tlsmail' gem" unless has_tlsmail_gem
237
- # Net::SMTP.enable_tls(OpenSSL::SSL::VERIFY_NONE)
238
- # send_email(email)
239
- # end
240
- end
241
-
242
- private
243
-
244
- def self.get_logger
245
- @@logger ||= Logger.new(STDOUT)
246
- end
247
-
248
150
  end
249
151
  end
@@ -1,5 +1,5 @@
1
1
  module Reptile
2
- # The runner class is responsible for running command on each slave.
2
+ # The runner class is responsible for running command on each slave.
3
3
  # The commands are run sequentially, no guaranteed order (though probably from yml file).
4
4
  class Runner
5
5
  # Set the user settings for a user that has REPLICATION SLAVE privilidgess
@@ -21,7 +21,7 @@ module Reptile
21
21
  # The databases to run commands upon.
22
22
  def self.databases
23
23
  @databases
24
- end
24
+ end
25
25
 
26
26
  # Set the slaves to run command upon.
27
27
  def self.slaves=(slaves)
@@ -42,18 +42,16 @@ module Reptile
42
42
  raise "You need to specify the slaves to run against!" if @slaves.nil?
43
43
  @slaves
44
44
  end
45
-
46
-
47
- # Tries to establish a database connection, and returns that connection.
45
+
46
+
47
+ # Tries to establish a database connection, and returns that connection.
48
48
  # Dumps configs on error.
49
49
  def self.connect(configs)
50
50
  ActiveRecord::Base.establish_connection(configs)
51
51
  ActiveRecord::Base.connection
52
52
  rescue Exception => e
53
- puts "****"
54
- puts "Error connecting to database: #{e}"
55
- puts "****"
56
- puts YAML::dump(configs)
53
+ Log.error "Error connecting to database: #{e}"
54
+ Log.error YAML::dump(configs)
57
55
  exit 1
58
56
  end
59
57
 
@@ -61,23 +59,23 @@ module Reptile
61
59
  # Takes an optional set of connection paramters to override defaults.
62
60
  def self.execute_on_slaves(cmd, configs={})
63
61
  slaves.each do |name, slave_configs|
64
- puts "Executing #{cmd} on #{name}"
65
- puts slave_configs.inspect
62
+ Log.info "Executing #{cmd} on #{name}"
63
+ Log.info slave_configs.inspect
66
64
  connection = connect(slave_configs.merge(user).merge(configs))
67
65
  connection.execute(cmd)
68
- end
66
+ end
69
67
  end
70
68
 
71
69
  # Execute STOP SLAVE on all slaves;
72
70
  def self.stop_slaves
73
71
  execute_on_slaves("STOP SLAVE;")
74
72
  end
75
-
73
+
76
74
  # Execute START SLAVE on all slaves.
77
75
  def self.start_slaves
78
76
  execute_on_slaves("START SLAVE;")
79
- end
80
-
77
+ end
78
+
81
79
  # Creates users with specific permissions on the different mysql servers, both masters and slaves.
82
80
  # Prompts for username and password of an account that has GRANT priviledges.
83
81
  def self.setup
@@ -90,21 +88,21 @@ module Reptile
90
88
  execute_on_slaves("GRANT select ON *.* TO #{ro_user}@???? INDENTIFIED BY #{ro_password}", grant_user_configs)
91
89
  execute_on_slaves("GRANT select ON *.* TO #{repl_user}@???? INDENTIFIED BY #{repl_password}", grant_user_configs)
92
90
  end
93
-
91
+
94
92
  def self.setup_heartbeat
95
93
  # MySQL DTD for setting up Heartbeats. Change HEARTBEAT_USER, HEARTBEAT_PASS, and MONITORING_BOX to ip of the monitoring server.
96
94
  # GRANT SELECT, INSERT, UPDATE, ALTER ON replication_monitor.* TO 'HEARTBEAT_USER'@"localhost" IDENTIFIED BY 'HEARTBEAT_PASS';
97
95
  # GRANT SELECT, INSERT, UPDATE, ALTER ON replication_monitor.* TO 'HEARTBEAT_USER'@"MONITORING_BOX" IDENTIFIED BY 'HEARTBEAT_PASS';
98
- #
96
+ #
99
97
  # CREATE DATABASE replication_monitor;
100
- #
98
+ #
101
99
  # CREATE TABLE replication_monitor.heartbeats (
102
- # unix_time INTEGER NOT NULL,
103
- # db_time TIMESTAMP NOT NULL,
100
+ # unix_time INTEGER NOT NULL,
101
+ # db_time TIMESTAMP NOT NULL,
104
102
  # INDEX time_idx(unix_time)
105
103
  # )
106
104
  end
107
-
105
+
108
106
  def self.test_connections
109
107
  databases.each do |db|
110
108
  databases.roles.each do |role|
@@ -0,0 +1,3 @@
1
+ module Reptile
2
+ VERSION = "0.1.0"
3
+ end
data/lib/reptile.rb CHANGED
@@ -1,5 +1,4 @@
1
- $:.unshift(File.dirname(__FILE__)) unless
2
- $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
1
+ $:.unshift(File.dirname(__FILE__)) unless $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
3
2
 
4
3
  require 'ostruct'
5
4
  require 'openssl'
@@ -7,6 +6,7 @@ require 'rubygems'
7
6
  require 'tlsmail'
8
7
  require 'net/smtp'
9
8
 
9
+ require 'reptile/log'
10
10
  require 'reptile/heartbeat'
11
11
  require 'reptile/delta_monitor'
12
12
  require 'reptile/replication_monitor'
data/reptile.gemspec ADDED
@@ -0,0 +1,31 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path("../lib/reptile/version", __FILE__)
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = "reptile"
6
+ s.version = Reptile::VERSION
7
+ s.platform = Gem::Platform::RUBY
8
+
9
+ s.authors = ["nick.stielau@gmail.com"]
10
+ s.date = "2010-01-05"
11
+ s.default_executable = "replication_status"
12
+ s.description = "Cold blooded mysql replication monitoring."
13
+ s.email = "nick.stielau@gmail.com"
14
+
15
+ s.files = `git ls-files`.split("\n")
16
+ s.executables = `git ls-files`.split("\n").map{|f| f =~ /^bin\/(.*)/ ? $1 : nil}.compact
17
+ s.require_path = 'lib'
18
+
19
+ s.homepage = %q{http://reptile.rubyforge.org/}
20
+ s.rdoc_options = ["--charset=UTF-8"]
21
+ s.require_paths = ["lib"]
22
+ s.required_rubygems_version = ">= 1.3.6"
23
+
24
+ s.summary = "Cold blooded mysql replication monitoring."
25
+
26
+ s.add_development_dependency "bundler", ">= 1.0.0"
27
+
28
+ s.add_runtime_dependency "activerecord", ">= 0"
29
+ s.add_runtime_dependency "mixlib-log", ">= 1.2.0"
30
+ end
31
+
@@ -1,4 +1,4 @@
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
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
2
 
3
3
  GRANT REPLICATION SLAVE, REPLICATION CLIENT, SUPER ON *.* TO 'REPLICATION_USER'@"localhost" IDENTIFIED BY 'REPLICATION_PASS';
4
4
  GRANT REPLICATION SLAVE, REPLICATION CLIENT, SUPER ON *.* TO 'REPLICATION_USER'@"MONITORING_BOX" IDENTIFIED BY 'REPLICATION_PASS';
@@ -12,7 +12,7 @@ GRANT SELECT, INSERT, UPDATE, ALTER ON replication_monitor.* TO 'HEARTBEAT_USER'
12
12
  CREATE DATABASE replication_monitor;
13
13
 
14
14
  CREATE TABLE replication_monitor.heartbeats (
15
- unix_time INTEGER NOT NULL,
16
- db_time TIMESTAMP NOT NULL,
15
+ unix_time INTEGER NOT NULL,
16
+ db_time TIMESTAMP NOT NULL,
17
17
  INDEX time_idx(unix_time)
18
18
  )
metadata CHANGED
@@ -1,10 +1,16 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: reptile
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.6
4
+ hash: 27
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 1
9
+ - 0
10
+ version: 0.1.0
5
11
  platform: ruby
6
12
  authors:
7
- - Nick Stielau
13
+ - nick.stielau@gmail.com
8
14
  autorequire:
9
15
  bindir: bin
10
16
  cert_chain: []
@@ -13,52 +19,80 @@ date: 2010-01-05 00:00:00 -08:00
13
19
  default_executable: replication_status
14
20
  dependencies:
15
21
  - !ruby/object:Gem::Dependency
16
- name: tlsmail
17
- type: :runtime
18
- version_requirement:
19
- version_requirements: !ruby/object:Gem::Requirement
22
+ name: bundler
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
20
26
  requirements:
21
27
  - - ">="
22
28
  - !ruby/object:Gem::Version
23
- version: "0"
24
- version:
29
+ hash: 23
30
+ segments:
31
+ - 1
32
+ - 0
33
+ - 0
34
+ version: 1.0.0
35
+ type: :development
36
+ version_requirements: *id001
25
37
  - !ruby/object:Gem::Dependency
26
38
  name: activerecord
27
- type: :runtime
28
- version_requirement:
29
- version_requirements: !ruby/object:Gem::Requirement
39
+ prerelease: false
40
+ requirement: &id002 !ruby/object:Gem::Requirement
41
+ none: false
30
42
  requirements:
31
43
  - - ">="
32
44
  - !ruby/object:Gem::Version
45
+ hash: 3
46
+ segments:
47
+ - 0
33
48
  version: "0"
34
- version:
49
+ type: :runtime
50
+ version_requirements: *id002
51
+ - !ruby/object:Gem::Dependency
52
+ name: mixlib-log
53
+ prerelease: false
54
+ requirement: &id003 !ruby/object:Gem::Requirement
55
+ none: false
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ hash: 31
60
+ segments:
61
+ - 1
62
+ - 2
63
+ - 0
64
+ version: 1.2.0
65
+ type: :runtime
66
+ version_requirements: *id003
35
67
  description: Cold blooded mysql replication monitoring.
36
68
  email: nick.stielau@gmail.com
37
69
  executables:
38
70
  - replication_status
39
71
  extensions: []
40
72
 
41
- extra_rdoc_files:
42
- - README.rdoc
73
+ extra_rdoc_files: []
74
+
43
75
  files:
44
76
  - .gitignore
45
- - History.txt
46
- - Manifest.txt
47
- - PostInstall.txt
77
+ - Gemfile
78
+ - Gemfile.lock
79
+ - LICENSE
48
80
  - README.rdoc
49
81
  - Rakefile
50
- - VERSION
51
82
  - bin/replication_status
52
83
  - lib/reptile.rb
53
84
  - lib/reptile/databases.rb
54
85
  - lib/reptile/delta_monitor.rb
55
- - lib/reptile/dtd.sql
56
86
  - lib/reptile/heartbeat.rb
87
+ - lib/reptile/log.rb
57
88
  - lib/reptile/replication_monitor.rb
58
89
  - lib/reptile/runner.rb
59
90
  - lib/reptile/status.rb
60
91
  - lib/reptile/users.rb
92
+ - lib/reptile/version.rb
61
93
  - replication.yml.sample
94
+ - reptile.gemspec
95
+ - reptile_setup.sql
62
96
  - script/console
63
97
  - script/destroy
64
98
  - script/generate
@@ -92,24 +126,31 @@ rdoc_options:
92
126
  require_paths:
93
127
  - lib
94
128
  required_ruby_version: !ruby/object:Gem::Requirement
129
+ none: false
95
130
  requirements:
96
131
  - - ">="
97
132
  - !ruby/object:Gem::Version
133
+ hash: 3
134
+ segments:
135
+ - 0
98
136
  version: "0"
99
- version:
100
137
  required_rubygems_version: !ruby/object:Gem::Requirement
138
+ none: false
101
139
  requirements:
102
140
  - - ">="
103
141
  - !ruby/object:Gem::Version
104
- version: "0"
105
- version:
142
+ hash: 23
143
+ segments:
144
+ - 1
145
+ - 3
146
+ - 6
147
+ version: 1.3.6
106
148
  requirements: []
107
149
 
108
150
  rubyforge_project:
109
- rubygems_version: 1.3.5
151
+ rubygems_version: 1.3.7
110
152
  signing_key:
111
153
  specification_version: 3
112
154
  summary: Cold blooded mysql replication monitoring.
113
- test_files:
114
- - test/test_helper.rb
115
- - test/test_reptile.rb
155
+ test_files: []
156
+
data/History.txt DELETED
@@ -1,4 +0,0 @@
1
- == 0.0.1 2009-06-24
2
-
3
- * 1 major enhancement:
4
- * Initial release
data/Manifest.txt DELETED
@@ -1,20 +0,0 @@
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 DELETED
@@ -1,5 +0,0 @@
1
-
2
- For more information on reptile, see http://reptile.rubyforge.org
3
-
4
-
5
-
data/VERSION DELETED
@@ -1 +0,0 @@
1
- 0.0.6