mys3ql 0.0.2 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -32,7 +32,7 @@ Second, create your config file:
32
32
 
33
33
  mysql:
34
34
  # Database to back up
35
- database: aircms_production
35
+ database:
36
36
  # MySql credentials
37
37
  user:
38
38
  password:
@@ -41,7 +41,7 @@ Second, create your config file:
41
41
  # If you are using MySql binary logging:
42
42
  # Path to the binary logs, should match the bin_log option in your my.cnf.
43
43
  # Comment out if you are not using mysql binary logging
44
- bin_log: /Users/andy/Desktop/mysql-bin
44
+ bin_log: /var/lib/mysql/binlog/mysql-bin
45
45
 
46
46
  s3:
47
47
  # S3 credentials
@@ -63,6 +63,10 @@ The MySQL user needs to have the RELOAD and the SUPER privileges, these can be g
63
63
  GRANT RELOAD ON *.* TO 'user_name'@'%' IDENTIFIED BY 'password';
64
64
  GRANT SUPER ON *.* TO 'user_name'@'%' IDENTIFIED BY 'password';
65
65
 
66
+ You may need to run mys3ql's incremental backup with special permissions (sudo), depending on the ownership of the binlogs directory.
67
+
68
+ N.B. the binary logs contain updates to all the databases on the server. This means you can only switch on incremental backups for one database per server, because the logs will be purged each time a database is dumped.
69
+
66
70
 
67
71
  ## Inspiration
68
72
 
@@ -72,8 +76,8 @@ Marc-André Cournoyer's [mysql_s3_backup](https://github.com/macournoyer/mysql_s
72
76
  ## To Do
73
77
 
74
78
  - tests ;)
75
- - restore (pull latest dump, pull bin files, pipe dump into mysql, pipe binfiles into mysql)
76
79
  - remove old dump files (s3)
80
+ - restore from non-latest dump
77
81
 
78
82
 
79
83
  ## Questions, Problems, Feedback
data/bin/mys3ql CHANGED
@@ -1,63 +1,35 @@
1
1
  #!/usr/bin/env ruby-local-exec
2
2
 
3
- # Hmm, is there a better way which doesn't force rubygems?
4
- require 'rubygems'
5
- require 'bundler/setup'
6
-
7
3
  lib_dir = File.join(File.dirname(__FILE__), '..', 'lib')
8
4
  $LOAD_PATH.unshift lib_dir if File.directory?(lib_dir)
9
5
 
10
6
  require 'mys3ql'
11
- require 'choice'
12
-
13
- Choice.options do
14
-
15
- header ''
16
- header 'Specific options:'
7
+ require 'main'
17
8
 
18
- option :full do
19
- short '-f'
20
- long '--full'
21
- desc 'Dump database, push to S3, and clean up bin logs on S3'
9
+ Main do
10
+ # consider using modes as/when we need command-specific arguments (e.g. restore specific backup)
11
+ argument 'command' do
12
+ validate { |command| %w[ full incremental restore ].include? command }
13
+ description 'full | incremental | restore'
22
14
  end
23
15
 
24
- option :incremental do
25
- short '-i'
26
- long '--incremental'
27
- desc "Push mysql's bin logs to S3"
16
+ option 'config', 'c' do
17
+ argument :required
18
+ description 'load configuration from YAML file'
19
+ defaults '~/.mys3ql'
28
20
  end
29
21
 
30
- option :config do
31
- short '-c'
32
- long '--config=FILE'
33
- desc 'Load configuration from YAML file (default: ~/.mys3ql)'
34
- default '~/.mys3ql'
22
+ option 'debug', 'd' do
23
+ description 'be verbose'
35
24
  end
36
25
 
37
- separator ''
38
- separator 'General options:'
39
-
40
- option :debug do
41
- short '-d'
42
- long '--debug'
43
- desc 'Turn on debugging mode'
44
- end
26
+ option 'version', 'v'
45
27
 
46
- option :help do
47
- long '--help'
48
- desc 'Show this message'
49
- end
50
-
51
- option :version do
52
- short '-v'
53
- long '--version'
54
- desc 'Show version'
55
- action do
28
+ def run
29
+ if params[:version]
56
30
  puts "mys3ql v#{Mys3ql::VERSION}"
57
31
  exit
58
32
  end
33
+ Mys3ql::Conductor.run params[:command].value, params[:config].value, params[:debug].value
59
34
  end
60
-
61
35
  end
62
-
63
- Mys3ql::Conductor.run Choice.choices
@@ -1,3 +1,4 @@
1
+ require 'tempfile'
1
2
  require 'mys3ql/config'
2
3
  require 'mys3ql/mysql'
3
4
  require 'mys3ql/s3'
@@ -5,15 +6,10 @@ require 'mys3ql/s3'
5
6
  module Mys3ql
6
7
  class Conductor
7
8
 
8
- def self.run(options)
9
- conductor = Conductor.new(options['config'])
10
- conductor.debug = options[:debug]
11
-
12
- if options['full']
13
- conductor.full
14
- elsif options['incremental']
15
- conductor.incremental
16
- end
9
+ def self.run(command, config, debug)
10
+ conductor = Conductor.new(config)
11
+ conductor.debug = debug
12
+ conductor.send command
17
13
  end
18
14
 
19
15
  def initialize(config_file = nil)
@@ -24,17 +20,49 @@ module Mys3ql
24
20
 
25
21
  def full
26
22
  @mysql.dump
27
- @s3.push_dump_to_s3 @mysql.dump_file
28
- @mysql.clean_up_dump
29
- @s3.delete_bin_logs_on_s3
23
+ @s3.store @mysql.dump_file
24
+ @mysql.delete_dump
25
+ @s3.delete_bin_logs
30
26
  end
31
27
 
32
28
  def incremental
33
- @s3.push_bin_logs_to_s3
29
+ @mysql.each_bin_log do |log|
30
+ @s3.store log, false
31
+ end
32
+ end
33
+
34
+ # for now only restore from latest
35
+ def restore
36
+ # get latest dump
37
+ with_temp_file do |file|
38
+ @s3.retrieve :latest, file
39
+ @mysql.restore file
40
+ end
41
+
42
+ # apply subsequent bin logs
43
+ @s3.each_bin_log do |log|
44
+ with_temp_file do |file|
45
+ @s3.retrieve log, file
46
+ @mysql.apply_bin_log file
47
+ end
48
+ end
49
+
50
+ # NOTE: not sure about this:
51
+ puts "You might want to flush mysql's logs..."
34
52
  end
35
53
 
36
54
  def debug=(val)
37
55
  @config.debug = val
38
56
  end
57
+
58
+ private
59
+
60
+ def with_temp_file(&block)
61
+ file = Tempfile.new 'mys3ql-sql'
62
+ yield file.path
63
+ nil
64
+ ensure
65
+ file.close!
66
+ end
39
67
  end
40
68
  end
@@ -8,22 +8,57 @@ module Mys3ql
8
8
  @config = config
9
9
  end
10
10
 
11
+ #
12
+ # dump
13
+ #
14
+
11
15
  def dump
12
- cmd = "#{@config.bin_path}mysqldump -u'#{@config.user}'"
13
- cmd += " -p'#{@config.password}'" if @config.password
14
- cmd += " --quick --single-transaction --create-options"
16
+ cmd = "#{@config.bin_path}mysqldump"
17
+ cmd += ' --quick --single-transaction --create-options'
15
18
  cmd += ' --flush-logs --master-data=2 --delete-master-logs' if binary_logging?
16
- cmd += " #{@config.database} | gzip > #{dump_file}"
17
- execute cmd
19
+ cmd += cli_options
20
+ cmd += " | gzip > #{dump_file}"
21
+ run cmd
22
+ end
23
+
24
+ def dump_file
25
+ @dump_file ||= "#{timestamp}.sql.gz"
18
26
  end
19
27
 
20
- def clean_up_dump
28
+ def delete_dump
21
29
  File.delete dump_file
22
- log "deleted #{dump_file}"
30
+ log "mysql: deleted #{dump_file}"
23
31
  end
24
32
 
25
- def dump_file
26
- @dump_file ||= "#{timestamp}.sql.gz"
33
+ #
34
+ # bin_logs
35
+ #
36
+
37
+ # flushes logs, yields each bar the last to the block
38
+ def each_bin_log(&block)
39
+ execute 'flush logs'
40
+ logs = Dir.glob("#{@config.bin_log}.[0-9]*").sort_by { |f| f[/\d+/].to_i }
41
+ logs_to_backup = logs[0..-2] # all logs except the last, which is in use
42
+ logs_to_backup.each do |log_file|
43
+ yield log_file
44
+ end
45
+ # delete binlogs from file system
46
+ #execute "purge master logs to '#{File.basename(logs[-1])}'"
47
+ end
48
+
49
+ #
50
+ # restore
51
+ #
52
+
53
+ def restore(file)
54
+ run "gunzip -c #{file} | #{@config.bin_path}mysql #{cli_options}"
55
+ end
56
+
57
+ def apply_bin_log(file)
58
+ cmd = "#{@config.bin_path}mysqlbinlog --database=#{@config.database} #{file}"
59
+ cmd += " | #{@config.bin_path}mysql -u'#{@config.user}'"
60
+ cmd += " -p'#{@config.password}'" if @config.password
61
+ run cmd
27
62
  end
28
63
 
29
64
  private
@@ -36,5 +71,15 @@ module Mys3ql
36
71
  @config.bin_log && @config.bin_log.length > 0
37
72
  end
38
73
 
74
+ def cli_options
75
+ cmd = " -u'#{@config.user}'"
76
+ cmd += " -p'#{@config.password}'" if @config.password
77
+ cmd += " #{@config.database}"
78
+ end
79
+
80
+ def execute(sql)
81
+ run %Q(#{@config.bin_path}mysql -e "#{sql}" #{cli_options})
82
+ end
83
+
39
84
  end
40
85
  end
@@ -9,35 +9,66 @@ module Mys3ql
9
9
  @config = config
10
10
  end
11
11
 
12
- def push_dump_to_s3(dump_file)
13
- key = "#{dumps_prefix}/#{dump_file}"
14
- copy_key = "#{dumps_prefix}/latest.sql.gz"
15
- s3_file = push_to_s3 dump_file, key
16
- if s3_file
12
+ def store(file, dump = true)
13
+ key = key_for(dump ? :dump : :bin_log, file)
14
+ s3_file = save file, key
15
+ if dump && s3_file
16
+ copy_key = key_for :latest
17
17
  s3_file.copy @config.bucket, copy_key
18
18
  log "copied #{key} to #{copy_key}"
19
19
  end
20
20
  end
21
21
 
22
- def push_bin_logs_to_s3
23
- if bin_logs_exist?
24
- Dir["#{@config.bin_log}/*"].each do |file|
25
- name = File.basename file
26
- key = "#{bin_logs_prefix}/#{name}"
27
- push_to_s3 file, key
28
- end
22
+ def delete_bin_logs
23
+ each_bin_log do |file|
24
+ file.destroy
25
+ log "s3: destroyed #{file.key}"
29
26
  end
30
27
  end
31
28
 
32
- def delete_bin_logs_on_s3
33
- bucket.files.all(:prefix => "#{bin_logs_prefix}").each do |file|
34
- file.destroy
35
- log "destroyed #{file.key}"
29
+ def each_bin_log(&block)
30
+ bucket.files.all(:prefix => "#{bin_logs_prefix}").sort_by { |file| file.key[/\d+/].to_i }.each do |file|
31
+ yield file
36
32
  end
37
33
  end
38
34
 
35
+ def retrieve(s3_file, local_file)
36
+ key = (s3_file == :latest) ? key_for(:latest) : s3_file.key
37
+ get key, local_file
38
+ end
39
+
39
40
  private
40
41
 
42
+ def get(s3_key, local_file_name)
43
+ s3_file = bucket.files.get s3_key
44
+ File.open(local_file_name, 'wb') do |file|
45
+ file.write s3_file.body
46
+ end
47
+ log "s3: pulled #{s3_key} to #{local_file_name}"
48
+ end
49
+
50
+ # returns Fog::Storage::AWS::File if we pushed, nil otherwise.
51
+ def save(local_file_name, s3_key)
52
+ unless bucket.files.head(s3_key)
53
+ s3_file = bucket.files.create(
54
+ :key => s3_key,
55
+ :body => File.open(local_file_name),
56
+ :public => false
57
+ )
58
+ log "s3: pushed #{local_file_name} to #{s3_key}"
59
+ s3_file
60
+ end
61
+ end
62
+
63
+ def key_for(kind, file = nil)
64
+ name = File.basename file if file
65
+ case kind
66
+ when :dump; "#{dumps_prefix}/#{name}"
67
+ when :bin_log; "#{bin_logs_prefix}/#{name}"
68
+ when :latest; "#{dumps_prefix}/latest.sql.gz"
69
+ end
70
+ end
71
+
41
72
  def s3
42
73
  @s3 ||= begin
43
74
  s = Fog::Storage.new(
@@ -45,32 +76,20 @@ module Mys3ql
45
76
  :aws_secret_access_key => @config.secret_access_key,
46
77
  :aws_access_key_id => @config.access_key_id
47
78
  )
48
- log 'connected to s3'
79
+ log 's3: connected'
49
80
  s
50
81
  end
51
82
  end
52
83
 
53
84
  def bucket
54
85
  @directory ||= begin
55
- d = s3.directories.get @config.bucket # assume bucket exists
56
- log "opened bucket #{@config.bucket}"
86
+ d = s3.directories.get @config.bucket
87
+ raise "S3 bucket #{@config.bucket} not found" unless d # create bucket instead?
88
+ log "s3: opened bucket #{@config.bucket}"
57
89
  d
58
90
  end
59
91
  end
60
92
 
61
- # returns Fog::Storage::AWS::File if we pushed, nil otherwise.
62
- def push_to_s3(local_file_name, s3_key)
63
- unless bucket.files.head(s3_key)
64
- s3_file = bucket.files.create(
65
- :key => s3_key,
66
- :body => File.open(local_file_name),
67
- :public => false
68
- )
69
- log "pushed #{local_file_name} to #{s3_key}"
70
- s3_file
71
- end
72
- end
73
-
74
93
  def dumps_prefix
75
94
  "#{@config.database}/dumps"
76
95
  end
@@ -2,7 +2,7 @@ module Mys3ql
2
2
  class ShellCommandError < RuntimeError ; end
3
3
 
4
4
  module Shell
5
- def execute(command)
5
+ def run(command)
6
6
  log command
7
7
  result = `#{command}`
8
8
  log "==> #{result}"
@@ -1,3 +1,3 @@
1
1
  module Mys3ql
2
- VERSION = "0.0.2"
2
+ VERSION = "0.0.4"
3
3
  end
@@ -18,7 +18,7 @@ Gem::Specification.new do |s|
18
18
  s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
19
  s.require_paths = ["lib"]
20
20
 
21
- s.add_dependency 'choice', '~> 0.1.4'
21
+ s.add_dependency 'main', '~> 4.8.0'
22
22
  s.add_dependency 'fog', '~> 1.0.0'
23
23
  s.add_development_dependency 'rake'
24
24
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mys3ql
3
3
  version: !ruby/object:Gem::Version
4
- hash: 27
4
+ hash: 23
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
8
  - 0
9
- - 2
10
- version: 0.0.2
9
+ - 4
10
+ version: 0.0.4
11
11
  platform: ruby
12
12
  authors:
13
13
  - Andy Stewart
@@ -15,29 +15,29 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2011-10-31 00:00:00 +01:00
18
+ date: 2011-11-03 00:00:00 +01:00
19
19
  default_executable:
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency
22
- name: choice
22
+ name: main
23
23
  prerelease: false
24
- requirement: &id001 !ruby/object:Gem::Requirement
24
+ version_requirements: &id001 !ruby/object:Gem::Requirement
25
25
  none: false
26
26
  requirements:
27
27
  - - ~>
28
28
  - !ruby/object:Gem::Version
29
- hash: 19
29
+ hash: 31
30
30
  segments:
31
- - 0
32
- - 1
33
31
  - 4
34
- version: 0.1.4
32
+ - 8
33
+ - 0
34
+ version: 4.8.0
35
35
  type: :runtime
36
- version_requirements: *id001
36
+ requirement: *id001
37
37
  - !ruby/object:Gem::Dependency
38
38
  name: fog
39
39
  prerelease: false
40
- requirement: &id002 !ruby/object:Gem::Requirement
40
+ version_requirements: &id002 !ruby/object:Gem::Requirement
41
41
  none: false
42
42
  requirements:
43
43
  - - ~>
@@ -49,11 +49,11 @@ dependencies:
49
49
  - 0
50
50
  version: 1.0.0
51
51
  type: :runtime
52
- version_requirements: *id002
52
+ requirement: *id002
53
53
  - !ruby/object:Gem::Dependency
54
54
  name: rake
55
55
  prerelease: false
56
- requirement: &id003 !ruby/object:Gem::Requirement
56
+ version_requirements: &id003 !ruby/object:Gem::Requirement
57
57
  none: false
58
58
  requirements:
59
59
  - - ">="
@@ -63,7 +63,7 @@ dependencies:
63
63
  - 0
64
64
  version: "0"
65
65
  type: :development
66
- version_requirements: *id003
66
+ requirement: *id003
67
67
  description: Simple backup of your MySql database onto Amazon S3.
68
68
  email:
69
69
  - boss@airbladesoftware.com