rds-s3-backup 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/.rspec +2 -0
- data/.rvmrc +48 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +29 -0
- data/Rakefile +1 -0
- data/bin/rds-s3-backup.rb +69 -0
- data/lib/rds-s3-backup.rb +132 -0
- data/lib/rds-s3-backup/datadog.rb +64 -0
- data/lib/rds-s3-backup/myrds.rb +153 -0
- data/lib/rds-s3-backup/mys3.rb +128 -0
- data/lib/rds-s3-backup/mysqlcmds.rb +128 -0
- data/lib/rds-s3-backup/version.rb +7 -0
- data/rds-s3-backup.gemspec +29 -0
- data/spec/rds-s3-backup_spec.rb +85 -0
- data/spec/spec_helper.rb +17 -0
- data/test_data/.gitignore +2 -0
- data/test_data/sample-config.yml +12 -0
- data/test_output/.gitkeep +0 -0
- metadata +212 -0
data/.gitignore
ADDED
data/.rspec
ADDED
data/.rvmrc
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
#!/usr/bin/env bash
|
2
|
+
|
3
|
+
# This is an RVM Project .rvmrc file, used to automatically load the ruby
|
4
|
+
# development environment upon cd'ing into the directory
|
5
|
+
|
6
|
+
# First we specify our desired <ruby>[@<gemset>], the @gemset name is optional,
|
7
|
+
# Only full ruby name is supported here, for short names use:
|
8
|
+
# echo "rvm use 1.9.3" > .rvmrc
|
9
|
+
environment_id="ruby-1.9.3-p194@rds_s3_backup"
|
10
|
+
|
11
|
+
# Uncomment the following lines if you want to verify rvm version per project
|
12
|
+
# rvmrc_rvm_version="1.16.20 ()" # 1.10.1 seams as a safe start
|
13
|
+
# eval "$(echo ${rvm_version}.${rvmrc_rvm_version} | awk -F. '{print "[[ "$1*65536+$2*256+$3" -ge "$4*65536+$5*256+$6" ]]"}' )" || {
|
14
|
+
# echo "This .rvmrc file requires at least RVM ${rvmrc_rvm_version}, aborting loading."
|
15
|
+
# return 1
|
16
|
+
# }
|
17
|
+
|
18
|
+
# First we attempt to load the desired environment directly from the environment
|
19
|
+
# file. This is very fast and efficient compared to running through the entire
|
20
|
+
# CLI and selector. If you want feedback on which environment was used then
|
21
|
+
# insert the word 'use' after --create as this triggers verbose mode.
|
22
|
+
if [[ -d "${rvm_path:-$HOME/.rvm}/environments"
|
23
|
+
&& -s "${rvm_path:-$HOME/.rvm}/environments/$environment_id" ]]
|
24
|
+
then
|
25
|
+
\. "${rvm_path:-$HOME/.rvm}/environments/$environment_id"
|
26
|
+
[[ -s "${rvm_path:-$HOME/.rvm}/hooks/after_use" ]] &&
|
27
|
+
\. "${rvm_path:-$HOME/.rvm}/hooks/after_use" || true
|
28
|
+
else
|
29
|
+
# If the environment file has not yet been created, use the RVM CLI to select.
|
30
|
+
rvm --create "$environment_id" || {
|
31
|
+
echo "Failed to create RVM environment '${environment_id}'."
|
32
|
+
return 1
|
33
|
+
}
|
34
|
+
fi
|
35
|
+
|
36
|
+
# If you use bundler, this might be useful to you:
|
37
|
+
if [[ -s Gemfile ]] && {
|
38
|
+
! builtin command -v bundle >/dev/null ||
|
39
|
+
builtin command -v bundle | GREP_OPTIONS= \grep $rvm_path/bin/bundle >/dev/null
|
40
|
+
}
|
41
|
+
then
|
42
|
+
printf "%b" "The rubygem 'bundler' is not installed. Installing it now.\n"
|
43
|
+
gem install bundler
|
44
|
+
fi
|
45
|
+
if [[ -s Gemfile ]] && builtin command -v bundle >/dev/null
|
46
|
+
then
|
47
|
+
bundle install | GREP_OPTIONS= \grep -vE '^Using|Your bundle is complete'
|
48
|
+
fi
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Tamara Temple
|
2
|
+
|
3
|
+
MIT License
|
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
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# Rds::S3::Backup
|
2
|
+
|
3
|
+
TODO: Write a gem description
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'rds-s3-backup'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install rds-s3-backup
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
TODO: Write usage instructions here
|
22
|
+
|
23
|
+
## Contributing
|
24
|
+
|
25
|
+
1. Fork it
|
26
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
27
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
28
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
29
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
@@ -0,0 +1,69 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
=begin
|
3
|
+
|
4
|
+
= rds-s3-backup-new.rb
|
5
|
+
|
6
|
+
*Copyright*:: (C) 2013 by Novu, LLC
|
7
|
+
*Since*:: 2013-02-25
|
8
|
+
|
9
|
+
== Description
|
10
|
+
|
11
|
+
Perform the nightly backup from production to S3, creating an obfuscated backup along the way
|
12
|
+
|
13
|
+
=end
|
14
|
+
|
15
|
+
require 'rubygems'
|
16
|
+
require 'thor'
|
17
|
+
|
18
|
+
require 'rds-s3-backup'
|
19
|
+
|
20
|
+
require 'fog'
|
21
|
+
|
22
|
+
require 'logger'
|
23
|
+
require 'dogapi'
|
24
|
+
|
25
|
+
class RdsS3Backup < Thor
|
26
|
+
|
27
|
+
desc "s3_dump", "Runs a mysqldump from a restored snapshot of the specified RDS instance, and uploads the dump to S3"
|
28
|
+
method_option :rds_instance_id
|
29
|
+
method_option :s3_bucket
|
30
|
+
method_option :backup_bucket
|
31
|
+
method_option :s3_prefix
|
32
|
+
method_option :aws_access_key_id
|
33
|
+
method_option :aws_secret_access_key
|
34
|
+
method_option :mysql_database
|
35
|
+
method_option :mysql_username
|
36
|
+
method_option :mysql_password
|
37
|
+
method_option :fog_timeout, :desc => 'Timeout in seconds for Fog requests (AWS connector)'
|
38
|
+
method_option :obfuscate_sql, :desc => 'Obfuscation Stored Procedure source'
|
39
|
+
method_option :dump_ttl, :desc => "Number of old dumps to keep."
|
40
|
+
method_option :dump_directory => "Where to store the temporary sql dump file."
|
41
|
+
method_option :config_file, :desc => "YAML file of defaults for any option. Options given during execution override these."
|
42
|
+
method_option :aws_region, :desc => "Region of your RDS server (and S3 storage, unless aws-s3-region is specified)."
|
43
|
+
method_option :aws_s3_region, :desc => "Region to store your S3 dumpfiles, if different from the RDS region"
|
44
|
+
method_option :db_subnet_group_name, :desc => "VPC RDS DB subnet"
|
45
|
+
method_option :db_instance_type, :desc => "DB Instance type"
|
46
|
+
method_option :instance_id, :desc => "Instance ID to create/mount EBS to"
|
47
|
+
method_option :log_level, :desc => "Set the level of verbosity. (debug|normal|warn|error|fatal)"
|
48
|
+
|
49
|
+
def s3_dump
|
50
|
+
thor_defaults = {
|
51
|
+
'backup_bucket' => 'novu-backups',
|
52
|
+
's3_prefix' => 'db_dumps',
|
53
|
+
'fog_timeout' => 30 * 60,
|
54
|
+
'obfuscate_sql' => '/usr/local/etc/obfuscate.sql',
|
55
|
+
'dump_directory' => '/mnt/secure',
|
56
|
+
'dump_ttl' => 0,
|
57
|
+
'aws_region' => 'us-east-1',
|
58
|
+
'aws_s3_region' => 'us-west-2',
|
59
|
+
'db_instance_type' => 'db.m1.small',
|
60
|
+
'log_level' => 'info'
|
61
|
+
}
|
62
|
+
|
63
|
+
|
64
|
+
Rds::S3::Backup.run(options,thor_defaults)
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
|
69
|
+
RdsS3Backup.start
|
@@ -0,0 +1,132 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'logger'
|
3
|
+
require "rds-s3-backup/version"
|
4
|
+
require "rds-s3-backup/datadog"
|
5
|
+
require "rds-s3-backup/myrds"
|
6
|
+
require "rds-s3-backup/mys3"
|
7
|
+
require "rds-s3-backup/mysqlcmds"
|
8
|
+
require 'debugger'
|
9
|
+
|
10
|
+
module Rds
|
11
|
+
module S3
|
12
|
+
module Backup
|
13
|
+
|
14
|
+
def process_options(thor_options,thor_defaults)
|
15
|
+
|
16
|
+
options = thor_defaults
|
17
|
+
|
18
|
+
if thor_options[:config_file] && File.exists?(thor_options[:config_file])
|
19
|
+
begin
|
20
|
+
options = options.merge(YAML.load(File.read(thor_options[:config_file])))
|
21
|
+
rescue Exception => e
|
22
|
+
raise "Unable to read and parse #{thor_options[:config_file]}: #{e.class}: #{e}"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
options.merge!(thor_options)
|
27
|
+
|
28
|
+
# Check for required options
|
29
|
+
missing_options = %w{rds_instance_id s3_bucket aws_access_key_id aws_secret_access_key mysql_database mysql_username mysql_password}.reduce([]) {|a, o| a << o unless options.has_key?(o); a}
|
30
|
+
|
31
|
+
raise "Missing required options #{missing_options.inspect} in either configuration or command line" if missing_options.count > 0
|
32
|
+
|
33
|
+
options['timestamp'] = Time.new.strftime('%Y-%m-%d-%H-%M-%S-%Z') # => "2013-02-25-19-28-55-CST"
|
34
|
+
|
35
|
+
options
|
36
|
+
end
|
37
|
+
|
38
|
+
def run(thor_options,thor_defaults)
|
39
|
+
|
40
|
+
@options = process_options(thor_options,thor_defaults)
|
41
|
+
$logger = Logger.new(STDOUT)
|
42
|
+
$logger.level = set_logger_level(@options["log_level"])
|
43
|
+
|
44
|
+
$dogger = DataDog.new(@options['data_dog_api_key'])
|
45
|
+
|
46
|
+
"Starting RDS-S3-Backup at #{@options['timestamp']}".tap do |t|
|
47
|
+
$logger.info t
|
48
|
+
$dogger.send t
|
49
|
+
end
|
50
|
+
|
51
|
+
|
52
|
+
$logger.debug "#{File.basename(__FILE__)}:#{__LINE__}: Running with options:"
|
53
|
+
debug_opts = @options.dup
|
54
|
+
debug_opts['aws_access_key_id'] = 'X'*10
|
55
|
+
debug_opts['aws_secret_access_key'] = 'Y'*15
|
56
|
+
debug_opts['mysql_password'] = "ZZY"*5
|
57
|
+
debug_opts['data_dog_api_key'] = 'XYZZY'*3
|
58
|
+
$logger.debug debug_opts.to_yaml
|
59
|
+
|
60
|
+
begin
|
61
|
+
|
62
|
+
$logger.info "Creating RDS and S3 Connections"
|
63
|
+
rds = MyRDS.new(@options)
|
64
|
+
s3 = MyS3.new(@options)
|
65
|
+
|
66
|
+
$logger.info "Restoring Database"
|
67
|
+
rds.restore_db()
|
68
|
+
|
69
|
+
$logger.info "Dumping and saving original database contents"
|
70
|
+
real_data_file = "#{rds.server.id}-mysqldump-#{@options['timestamp']}.sql.gz"
|
71
|
+
s3.save_production(rds.dump(real_data_file))
|
72
|
+
|
73
|
+
if @options['dump_ttl'] > 0
|
74
|
+
$logger.info "Pruning old dumps"
|
75
|
+
s3.prune_files(:prefix => "#{rds.server.id}-mysqldump-",
|
76
|
+
:keep => @options['dump_ttl'])
|
77
|
+
end
|
78
|
+
|
79
|
+
$logger.info "Obfuscating database"
|
80
|
+
rds.obfuscate()
|
81
|
+
|
82
|
+
$logger.info "Dumping and saving obfuscated database contents"
|
83
|
+
clean_data_file = "clean-mysqldump.sql.gz"
|
84
|
+
s3.save_clean(rds.dump(clean_data_file))
|
85
|
+
|
86
|
+
rescue Exception => e
|
87
|
+
msg = "ERROR in #{File.basename(__FILE__)}: #{e.class}: #{e}\n"
|
88
|
+
msg << e.backtrace.join("\n")
|
89
|
+
$logger.error msg
|
90
|
+
$dogger.send msg unless e.is_a?(DataDogException) # don't make a loop!
|
91
|
+
raise e
|
92
|
+
ensure
|
93
|
+
cleanup(rds, s3, [ real_data_file, clean_data_file ])
|
94
|
+
end
|
95
|
+
|
96
|
+
"RDS-S3-Backup Completed Successfully".tap do |t|
|
97
|
+
$logger.info t
|
98
|
+
$dogger.send t, :alert_type => 'Info', :priority => 'low'
|
99
|
+
end
|
100
|
+
|
101
|
+
0 # exit code 0
|
102
|
+
|
103
|
+
|
104
|
+
end
|
105
|
+
|
106
|
+
def cleanup(rds, s3, unlink_files=[])
|
107
|
+
rds.destroy() unless rds.nil?
|
108
|
+
s3.destroy() unless s3.nil?
|
109
|
+
|
110
|
+
unlink_files = [ unlink_files ] unless unlink_files.is_a?(Array)
|
111
|
+
unlink_files = unlink_files.flatten
|
112
|
+
unlink_files.each {|f| File.unlink(f) if !f.nil? && File.exists?(f) }
|
113
|
+
|
114
|
+
end
|
115
|
+
|
116
|
+
def set_logger_level(ll)
|
117
|
+
case ll.downcase
|
118
|
+
when 'debug' ; Logger::DEBUG
|
119
|
+
when 'info' ; Logger::INFO
|
120
|
+
when 'warn' ; Logger::WARN
|
121
|
+
when 'error' ; Logger::ERROR
|
122
|
+
when 'fatal' ; Logger::FATAL
|
123
|
+
else
|
124
|
+
Logger::INFO
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
module_function :process_options, :run, :cleanup, :set_logger_level
|
129
|
+
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
=begin
|
2
|
+
|
3
|
+
= datadog.rb
|
4
|
+
|
5
|
+
*Copyright*:: (C) 2013 by Novu, LLC
|
6
|
+
*Author(s)*:: Tamara Temple <tamara.temple@novu.com>
|
7
|
+
*Since*:: 2013-02-26
|
8
|
+
*License*:: GPLv3
|
9
|
+
*Version*:: 0.0.1
|
10
|
+
|
11
|
+
== Description
|
12
|
+
|
13
|
+
=end
|
14
|
+
|
15
|
+
module Rds
|
16
|
+
module S3
|
17
|
+
module Backup
|
18
|
+
class DataDogException < RuntimeError ; end
|
19
|
+
class DataDog
|
20
|
+
|
21
|
+
def initialize(api_key)
|
22
|
+
raise DataDogException.new("No DataDog API given") if api_key.nil? || api_key.empty?
|
23
|
+
@api_key = api_key
|
24
|
+
end
|
25
|
+
|
26
|
+
def make_dog_client()
|
27
|
+
begin
|
28
|
+
Dogapi::Client.new(@api_key)
|
29
|
+
rescue Exception => e
|
30
|
+
unless e.is_a?(DataDogException) # no loops!
|
31
|
+
raise DataDogException.new "Error setting up DataDog connection: #{e.class}: #{e}"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
def send(msg, o={})
|
38
|
+
return # do nothing for testing
|
39
|
+
|
40
|
+
return if msg.nil? || msg.empty?
|
41
|
+
|
42
|
+
options = {
|
43
|
+
:msg_title => 'Alert',
|
44
|
+
:alert_type => 'Error',
|
45
|
+
:tags => [ "host:#{`hostname`}", "env:production" ],
|
46
|
+
:priority => 'normal',
|
47
|
+
:source => 'My Apps'}.merge(o)
|
48
|
+
|
49
|
+
@dog_client ||= make_dog_client()
|
50
|
+
|
51
|
+
begin
|
52
|
+
@dog_client.emit_event(Dogapi::Event.new(msg, options))
|
53
|
+
rescue Exception => e
|
54
|
+
unless e.is_a?(DataDogException) # no loops!
|
55
|
+
raise DataDogException.new "Error sending #{msg} to DataDog with options #{options.inspect}: #{e.class}: #{e.message}"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,153 @@
|
|
1
|
+
=begin
|
2
|
+
|
3
|
+
= rds.rb
|
4
|
+
|
5
|
+
*Copyright*:: (C) 2013 by Novu, LLC
|
6
|
+
*Author(s)*:: Tamara Temple <tamara.temple@novu.com>
|
7
|
+
*Since*:: 2013-02-26
|
8
|
+
*License*:: GPLv3
|
9
|
+
*Version*:: 0.0.1
|
10
|
+
|
11
|
+
== Description
|
12
|
+
|
13
|
+
=end
|
14
|
+
|
15
|
+
require 'fog'
|
16
|
+
|
17
|
+
module Rds::S3::Backup
|
18
|
+
|
19
|
+
|
20
|
+
class MyRDSException < RuntimeError ; end
|
21
|
+
class MyRDS
|
22
|
+
|
23
|
+
attr_accessor :rds, :server
|
24
|
+
|
25
|
+
def initialize(opts)
|
26
|
+
@opts = opts
|
27
|
+
|
28
|
+
@rds = get_rds_connection(:aws_access_key_id => @opts['aws_access_key_id'],
|
29
|
+
:aws_secret_access_key => @opts['aws_secret_access_key'],
|
30
|
+
:region => @opts['aws_region'])
|
31
|
+
|
32
|
+
@server = get_rds_server(@opts['rds_instance_id'])
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
def restore_db
|
37
|
+
|
38
|
+
begin
|
39
|
+
@rds.restore_db_instance_from_db_snapshot(new_snap.id,
|
40
|
+
backup_server_id,
|
41
|
+
{"DBSubnetGroupName" => @opts['db_subnet_group_name'],
|
42
|
+
"DBInstanceClass" => @opts['db_instance_type'] } )
|
43
|
+
rescue Exception => e
|
44
|
+
raise MyRDSException.new("Error in #{self.class}:restore_db: #{e.class}: #{e}")
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
|
50
|
+
def dump(backup_file_name)
|
51
|
+
@mysqlcmds ||= ::Rds::S3::Backup::MySqlCmds.new(backup_server.endpoint['Address'],
|
52
|
+
@opts['mysql_username'],
|
53
|
+
@opts['mysql_password'],
|
54
|
+
@opts['mysql_database'])
|
55
|
+
|
56
|
+
|
57
|
+
|
58
|
+
|
59
|
+
@mysqlcmds.dump(backup_file_path(backup_file_name)) # returns the dump file path
|
60
|
+
end
|
61
|
+
|
62
|
+
def obfuscate
|
63
|
+
@mysqlcmds ||= ::Rds::S3::Backup::MySqlCmds.new(backup_server.endpoint['Address'],
|
64
|
+
@opts['mysql_username'],
|
65
|
+
@opts['mysql_password'],
|
66
|
+
@opts['mysql_database'])
|
67
|
+
|
68
|
+
|
69
|
+
|
70
|
+
|
71
|
+
@mysqlcmds.exec(@opts['obfuscate_sql'])
|
72
|
+
end
|
73
|
+
|
74
|
+
def backup_file_path(backup_file_name)
|
75
|
+
File.join(@opts['dump_directory'], backup_file_name)
|
76
|
+
end
|
77
|
+
|
78
|
+
def backup_server_id
|
79
|
+
@backup_server_id ||= "#{@server.id}-s3-dump-server-#{@opts['timestamp']}"
|
80
|
+
end
|
81
|
+
|
82
|
+
def new_snap
|
83
|
+
unless @new_snap
|
84
|
+
self.server.snapshots.new(:id => snap_name).save
|
85
|
+
@new_snap = self.server.snapshots.get(snap_name)
|
86
|
+
|
87
|
+
(0..1).each {|i| $logger.debug "#{__FILE__}:#{__LINE__}: Waiting for new_snap server to be ready: #{i}"; @new_snap.wait_for { ready? } } # ready? sometimes lies
|
88
|
+
|
89
|
+
end
|
90
|
+
|
91
|
+
@new_snap
|
92
|
+
end
|
93
|
+
|
94
|
+
def backup_server
|
95
|
+
@backup_server ||= get_rds_server(backup_server_id)
|
96
|
+
|
97
|
+
(0..1).each {|i| $logger.debug "#{__FILE__}:#{__LINE__}: Waiting for backup_server to be ready: #{i}" ; @backup_server.wait_for {ready? } } # won't get fooled again!
|
98
|
+
@backup_server
|
99
|
+
|
100
|
+
end
|
101
|
+
|
102
|
+
def snap_name
|
103
|
+
@snap_name ||= "s3-dump-snap-#{@opts['timestamp']}"
|
104
|
+
end
|
105
|
+
|
106
|
+
def get_rds_connection(opts={})
|
107
|
+
options = {
|
108
|
+
:aws_access_key_id => @opts['aws_access_key_id'],
|
109
|
+
:aws_secret_access_key => @opts['aws_secret_access_key'],
|
110
|
+
:region => @opts['aws_region']}.merge(opts)
|
111
|
+
Fog.timeout=@opts['fog_timeout']
|
112
|
+
begin
|
113
|
+
connection = Fog::AWS::RDS.new(options)
|
114
|
+
rescue Exception => e
|
115
|
+
raise MyRDSException.new("Error in #{self.class}#get_rds_connection: #{e.class}: #{e}")
|
116
|
+
end
|
117
|
+
|
118
|
+
raise MyRDSException.new("Unable to make RDS connection") if connection.nil?
|
119
|
+
connection
|
120
|
+
|
121
|
+
|
122
|
+
end
|
123
|
+
|
124
|
+
def get_rds_server(id)
|
125
|
+
begin
|
126
|
+
server = @rds.servers.get(id)
|
127
|
+
rescue Exception => e
|
128
|
+
raise MyRDSException.new("Error getting server in #{self.class}#get_rds_server: #{e.class}: #{e}")
|
129
|
+
end
|
130
|
+
raise MyRDSException.new("Server is nil for #{id}") if server.nil?
|
131
|
+
server
|
132
|
+
end
|
133
|
+
|
134
|
+
|
135
|
+
def destroy
|
136
|
+
|
137
|
+
unless @new_snap.nil?
|
138
|
+
(0..1).each {|i| $logger.debug "#{__FILE__}:#{__LINE__}: Waiting for new_snap server to be ready in #{self.class}#destroy: #{i}"; @new_snap.wait_for { ready? } }
|
139
|
+
@new_snap.destroy
|
140
|
+
end
|
141
|
+
|
142
|
+
unless @backup_server.nil?
|
143
|
+
(0..1).each {|i| $logger.debug "#{__FILE__}:#{__LINE__}: Waiting for backup server to be ready in #{self.class}#destroy: #{i}"; @backup_server.wait_for {ready? } }
|
144
|
+
@backup_server.destroy(nil)
|
145
|
+
end
|
146
|
+
|
147
|
+
end
|
148
|
+
|
149
|
+
|
150
|
+
|
151
|
+
end
|
152
|
+
|
153
|
+
end
|
@@ -0,0 +1,128 @@
|
|
1
|
+
=begin
|
2
|
+
|
3
|
+
= mys3.rb
|
4
|
+
|
5
|
+
*Copyright*:: (C) 2013 by Novu, LLC
|
6
|
+
*Author(s)*:: Tamara Temple <tamara.temple@novu.com>
|
7
|
+
*Since*:: 2013-02-26
|
8
|
+
*License*:: GPLv3
|
9
|
+
*Version*:: 0.0.1
|
10
|
+
|
11
|
+
== Description
|
12
|
+
|
13
|
+
=end
|
14
|
+
|
15
|
+
module Rds::S3::Backup
|
16
|
+
class MyS3Exception < RuntimeError ; end
|
17
|
+
class MyS3
|
18
|
+
|
19
|
+
def initialize(options)
|
20
|
+
@options = options
|
21
|
+
|
22
|
+
@s3 = get_storage(:aws_access_key_id => @options['aws_access_key_id'],
|
23
|
+
:aws_secret_access_key => @options['aws_secret_access_key'],
|
24
|
+
:region => @options['aws_s3_region'] ||= @options['aws_region'])
|
25
|
+
end
|
26
|
+
|
27
|
+
def get_storage(o={})
|
28
|
+
options = {
|
29
|
+
:aws_access_key_id => nil,
|
30
|
+
:aws_secret_access_key => nil,
|
31
|
+
:region => nil,
|
32
|
+
:provider => 'AWS',
|
33
|
+
:scheme => 'https'}.merge(o)
|
34
|
+
|
35
|
+
begin
|
36
|
+
Fog::Storage.new(options)
|
37
|
+
rescue Exception => e
|
38
|
+
raise MyS3Exception.new "Error establishing storage connection: #{e.class}: #{e}"
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
def get_bucket(bucket)
|
44
|
+
|
45
|
+
begin
|
46
|
+
@s3.directories.get(bucket)
|
47
|
+
rescue Exception => e
|
48
|
+
raise MyS3Exception.new "Error getting bucket #{bucket} in S3: #{e.class}: #{e}"
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
|
53
|
+
def save_production(file_path)
|
54
|
+
save(s3_bucket, file_path, :acl => 'private')
|
55
|
+
end
|
56
|
+
|
57
|
+
def save_clean(file_path)
|
58
|
+
save(backup_bucket, file_path, :acl => 'authenticated-read')
|
59
|
+
end
|
60
|
+
|
61
|
+
|
62
|
+
|
63
|
+
def save(bucket, file_path, o={})
|
64
|
+
options = {
|
65
|
+
:key => File.join(@options['s3_prefix'], File.basename(file_path)),
|
66
|
+
:body => File.open(file_path),
|
67
|
+
:acl => 'authenticated-read',
|
68
|
+
:encryption => 'AES256',
|
69
|
+
:content_type => 'application/x-gzip'}.merge(o)
|
70
|
+
|
71
|
+
tries = 0
|
72
|
+
begin
|
73
|
+
|
74
|
+
bucket.files.new(options).save
|
75
|
+
|
76
|
+
rescue Exception => e
|
77
|
+
if tries < 3
|
78
|
+
@logger.info "Retrying S3 upload after #{tries} tries"
|
79
|
+
tries += 1
|
80
|
+
retry
|
81
|
+
else
|
82
|
+
raise MyS3Exception.new "Could not save #{File.basename(file_path)} to S3 after 3 tries: #{e.class}: #{e}"
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
|
88
|
+
|
89
|
+
def s3_bucket
|
90
|
+
@s3_bucket ||= get_bucket(@options['s3_bucket'])
|
91
|
+
end
|
92
|
+
|
93
|
+
def backup_bucket
|
94
|
+
@backup_bucket ||= get_bucket(@options['backup_bucket'])
|
95
|
+
end
|
96
|
+
|
97
|
+
|
98
|
+
def prune_files(o={})
|
99
|
+
options = {
|
100
|
+
:prefix => '',
|
101
|
+
:keep => 1
|
102
|
+
}.merge(o)
|
103
|
+
|
104
|
+
return if options[:keep] < 1 # must keep at least one, the last one!
|
105
|
+
|
106
|
+
my_files = s3_bucket.files.all('prefix' => options[:prefix])
|
107
|
+
return if my_files.nil?
|
108
|
+
|
109
|
+
if my_files.count > options[:keep]
|
110
|
+
my_files.
|
111
|
+
sort {|x,y| x.last_modified <=> y.last_modified}.
|
112
|
+
take(files_by_date.count - options[:keep]).
|
113
|
+
each do |f|
|
114
|
+
logger.info "Deleting #{f.name}"
|
115
|
+
f.destroy
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def destroy
|
121
|
+
# nothing really to do here...
|
122
|
+
end
|
123
|
+
|
124
|
+
|
125
|
+
end
|
126
|
+
|
127
|
+
|
128
|
+
end
|
@@ -0,0 +1,128 @@
|
|
1
|
+
=begin
|
2
|
+
|
3
|
+
= mysqldcmds.rb
|
4
|
+
|
5
|
+
*Copyright*:: (C) 2013 by Novu, LLC
|
6
|
+
*Author(s)*:: Tamara Temple <tamara.temple@novu.com>
|
7
|
+
*Since*:: 2013-02-26
|
8
|
+
*License*:: GPLv3
|
9
|
+
*Version*:: 0.0.1
|
10
|
+
|
11
|
+
== Description
|
12
|
+
|
13
|
+
=end
|
14
|
+
|
15
|
+
require 'open3'
|
16
|
+
|
17
|
+
module Rds::S3::Backup
|
18
|
+
|
19
|
+
class MySqlCmdsException < RuntimeError ; end
|
20
|
+
class MySqlCmds
|
21
|
+
attr_accessor :host, :user, :passwd, :dbname
|
22
|
+
|
23
|
+
def initialize(host, user, passwd, dbname)
|
24
|
+
|
25
|
+
# Assert real parameters
|
26
|
+
raise MySqlCmdsException.new("host is empty") if host.nil? || host.empty?
|
27
|
+
raise MySqlCmdsException.new("user is empty") if user.nil? || user.empty?
|
28
|
+
raise MySqlCmdsException.new("passwd is empty") if passwd.nil? || passwd.empty?
|
29
|
+
raise MySqlCmdsException.new("dbname is empty") if dbname.nil? || dbname.empty?
|
30
|
+
|
31
|
+
# Store instance vars
|
32
|
+
@host = host
|
33
|
+
@user = user
|
34
|
+
@passwd = passwd
|
35
|
+
@dbname = dbname
|
36
|
+
|
37
|
+
# Find commands to execute
|
38
|
+
@mysqldump = `which mysqldump`.chomp
|
39
|
+
raise MySqlCmdsException.new("No mysqldump command found") if @mysqldump.empty?
|
40
|
+
@mysql = `which mysql`.chomp
|
41
|
+
raise MySqlCmdsException.new("No mysql command found") if @mysql.empty?
|
42
|
+
@gzip = `which gzip`.chomp
|
43
|
+
raise MySqlCmdsException.new("No gzip command found") if @gzip.empty?
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
def dump(output)
|
48
|
+
raise MySqlCmdsException.new("output is not specified") if output.nil? || output.empty?
|
49
|
+
|
50
|
+
dump_opts = ["--opt",
|
51
|
+
"--add-drop-table",
|
52
|
+
"--single-transaction",
|
53
|
+
"--order-by-primary",
|
54
|
+
"--host=#{@host}",
|
55
|
+
"--user=#{@user}",
|
56
|
+
"--password=XXPASSWORDXX",
|
57
|
+
@dbname]
|
58
|
+
|
59
|
+
gzip_opts = ["--fast",
|
60
|
+
"--stdout",
|
61
|
+
"> #{output}"]
|
62
|
+
|
63
|
+
|
64
|
+
cmd = "#{@mysqldump} #{dump_opts.join(' ')} | #{@gzip} #{gzip_opts.join(' ')}"
|
65
|
+
|
66
|
+
run(cmd.gsub(/XXPASSWORDXX/,@passwd))
|
67
|
+
|
68
|
+
output # return the dump file path
|
69
|
+
|
70
|
+
end
|
71
|
+
|
72
|
+
def exec(script)
|
73
|
+
|
74
|
+
return if script.nil? || script.empty?
|
75
|
+
raise MySqlCmdsException.new("#{script} is not readable!") unless File.readable?(script)
|
76
|
+
|
77
|
+
obf_opts = ["--host=#{@host}",
|
78
|
+
"--user=#{user}",
|
79
|
+
"--password=XXPASSWORDXX",
|
80
|
+
@dbname]
|
81
|
+
|
82
|
+
|
83
|
+
cmd = "#{@mysql} #{obf_opts.join(' ')} < #{script}"
|
84
|
+
|
85
|
+
run(cmd.gsub(/XXPASSWORDXX/,@passwd))
|
86
|
+
|
87
|
+
end
|
88
|
+
|
89
|
+
def run(cmd)
|
90
|
+
|
91
|
+
result = _really_run_it(cmd)
|
92
|
+
if result.code != 0
|
93
|
+
error = "Mysql operation failed on mysql://#{@user}@#{@host}/#{@dbname}: Error code: #{result.code}.\n"
|
94
|
+
error << "Command:\n#{cmd}\n"
|
95
|
+
error << "stdout:\n#{result.stdout.join("\n")}\n"
|
96
|
+
error << "stderr:\n#{result.stderr.join("\n")}\n"
|
97
|
+
raise MySqlCmdsException.new error
|
98
|
+
end
|
99
|
+
|
100
|
+
|
101
|
+
end
|
102
|
+
|
103
|
+
Result = Struct.new :stdout, :stderr, :code
|
104
|
+
|
105
|
+
def _really_run_it(cmd)
|
106
|
+
result = Result.new
|
107
|
+
result.stdout = Array.new
|
108
|
+
result.stderr = Array.new
|
109
|
+
Open3.popen3(cmd) do |stdin, stdout, stderr, wait_thr|
|
110
|
+
stdin.close
|
111
|
+
until stdout.eof
|
112
|
+
result.stdout << stdout.gets.chomp
|
113
|
+
end
|
114
|
+
stdout.close
|
115
|
+
until stderr.eof
|
116
|
+
result.stderr << stderr.gets.chomp
|
117
|
+
end
|
118
|
+
stderr.close
|
119
|
+
result.code = wait_thr.value.exitstatus
|
120
|
+
end
|
121
|
+
result
|
122
|
+
end
|
123
|
+
|
124
|
+
end
|
125
|
+
|
126
|
+
|
127
|
+
|
128
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
lib = File.expand_path('../lib', __FILE__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
require 'rds-s3-backup/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |gem|
|
6
|
+
gem.name = "rds-s3-backup"
|
7
|
+
gem.version = Rds::S3::Backup::VERSION
|
8
|
+
gem.authors = ["Tamara Temple"]
|
9
|
+
gem.email = ["tamouse@gmail.com"]
|
10
|
+
gem.description = %q{"Backup from AWS RDS snapshot to AWS S3 as mysqldump"}
|
11
|
+
gem.summary = %q{"Backup from AWS RDS snapshot to AWS S3 as mysqldump"}
|
12
|
+
gem.homepage = ""
|
13
|
+
|
14
|
+
gem.files = `git ls-files`.split($/)
|
15
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
16
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
17
|
+
gem.require_paths = ["lib"]
|
18
|
+
|
19
|
+
gem.add_dependency('thor')
|
20
|
+
gem.add_dependency('fog')
|
21
|
+
gem.add_dependency('logger')
|
22
|
+
gem.add_dependency('dogapi')
|
23
|
+
gem.add_development_dependency('rspec')
|
24
|
+
gem.add_development_dependency('aruba')
|
25
|
+
gem.add_development_dependency('cucumber')
|
26
|
+
gem.add_development_dependency('rake')
|
27
|
+
gem.add_development_dependency('debugger')
|
28
|
+
|
29
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
=begin
|
2
|
+
|
3
|
+
= rds-s3-backup_spec.rb
|
4
|
+
|
5
|
+
*Copyright*:: (C) 2013 by Novu, LLC
|
6
|
+
*Author(s)*:: Tamara Temple <tamara.temple@novu.com>
|
7
|
+
*Since*:: 2013-02-26
|
8
|
+
*License*:: GPLv3
|
9
|
+
*Version*:: 0.0.1
|
10
|
+
|
11
|
+
== Description
|
12
|
+
|
13
|
+
=end
|
14
|
+
|
15
|
+
require 'rds-s3-backup'
|
16
|
+
|
17
|
+
|
18
|
+
module Rds::S3::Backup
|
19
|
+
|
20
|
+
describe "has version" do
|
21
|
+
it {Rds::S3::Backup::VERSION.should == '0.0.1' }
|
22
|
+
end
|
23
|
+
|
24
|
+
describe "module methods" do
|
25
|
+
|
26
|
+
it { Rds::S3::Backup.should respond_to(:run) }
|
27
|
+
it { Rds::S3::Backup.should respond_to(:process_options) }
|
28
|
+
it { Rds::S3::Backup.should respond_to(:cleanup) }
|
29
|
+
it { Rds::S3::Backup.should respond_to(:set_logger_level) }
|
30
|
+
end
|
31
|
+
|
32
|
+
describe "#run method" do
|
33
|
+
let(:options) { {
|
34
|
+
:rds_instance_id => 'stagingdb',
|
35
|
+
:s3_bucket => 'novu-backups',
|
36
|
+
:backup_bucket => 'novu-backups',
|
37
|
+
:s3_prefix => 'db_dumps',
|
38
|
+
:aws_access_key_id => 'ABCDE',
|
39
|
+
:aws_secret_access_key => '0987654321',
|
40
|
+
:mysql_database => 'novu_text',
|
41
|
+
:mysql_username => 'novurun',
|
42
|
+
:mysql_password => 'passw0rd',
|
43
|
+
:obfuscate_sql => '/usr/local/etc/obfuscate.sql',
|
44
|
+
:dump_ttl => 0,
|
45
|
+
:dump_directory => '/mnt/secure',
|
46
|
+
:aws_region => 'us-east-1',
|
47
|
+
:aws_s3_region => 'us-west-2',
|
48
|
+
:db_subnet_group_name => 'staging db subnet',
|
49
|
+
:db_instance_type => 'db.m1.small',
|
50
|
+
:instance_id => '',
|
51
|
+
:log_level => 0,
|
52
|
+
:quiet => false,
|
53
|
+
:verbose => true
|
54
|
+
} }
|
55
|
+
|
56
|
+
|
57
|
+
before(:each) do
|
58
|
+
DataDog.stub(:new).and_return(OpenStruct.new(:send => true))
|
59
|
+
|
60
|
+
MyRDS.stub(:new).and_return(OpenStruct.new(:restore_db => true,
|
61
|
+
:dump => Proc.new {|file| file },
|
62
|
+
:obfuscate => true,
|
63
|
+
:destroy => true,
|
64
|
+
:server =>
|
65
|
+
OpenStruct.new(:id => 1)
|
66
|
+
))
|
67
|
+
MyS3.stub(:new).and_return(OpenStruct.new(:save_production => true,
|
68
|
+
:save_clean => true,
|
69
|
+
:prune_files => true,
|
70
|
+
:destroy => true))
|
71
|
+
end
|
72
|
+
|
73
|
+
it "should process options" do
|
74
|
+
opts = Rds::S3::Backup.process_options(options)
|
75
|
+
opts.should be_a(Hash)
|
76
|
+
opts.should have_key(:timestamp)
|
77
|
+
opts[:timestamp].should_not be_nil
|
78
|
+
end
|
79
|
+
|
80
|
+
|
81
|
+
it { Rds::S3::Backup.run(options).should == 0 }
|
82
|
+
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
# This file was generated by the `rspec --init` command. Conventionally, all
|
2
|
+
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
|
3
|
+
# Require this file using `require "spec_helper"` to ensure that it is only
|
4
|
+
# loaded once.
|
5
|
+
#
|
6
|
+
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
7
|
+
RSpec.configure do |config|
|
8
|
+
config.treat_symbols_as_metadata_keys_with_true_values = true
|
9
|
+
config.run_all_when_everything_filtered = true
|
10
|
+
config.filter_run :focus
|
11
|
+
|
12
|
+
# Run specs in random order to surface order dependencies. If you find an
|
13
|
+
# order dependency and want to debug it, you can fix the order by providing
|
14
|
+
# the seed, which is printed after each run.
|
15
|
+
# --seed 1234
|
16
|
+
config.order = 'random'
|
17
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
rds_instance_id:
|
2
|
+
db_subnet_group_name:
|
3
|
+
s3_bucket:
|
4
|
+
aws_access_key_id:
|
5
|
+
aws_secret_access_key:
|
6
|
+
mysql_database:
|
7
|
+
mysql_username:
|
8
|
+
mysql_password:
|
9
|
+
dump_ttl: 0
|
10
|
+
data_dog_api_key:
|
11
|
+
obfuscate_sql: test_data/obfuscate.sql
|
12
|
+
dump_directory: test_output
|
File without changes
|
metadata
ADDED
@@ -0,0 +1,212 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rds-s3-backup
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Tamara Temple
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-03-02 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: thor
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: fog
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: logger
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: dogapi
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
type: :runtime
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: rspec
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ! '>='
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
86
|
+
type: :development
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ! '>='
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
94
|
+
- !ruby/object:Gem::Dependency
|
95
|
+
name: aruba
|
96
|
+
requirement: !ruby/object:Gem::Requirement
|
97
|
+
none: false
|
98
|
+
requirements:
|
99
|
+
- - ! '>='
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0'
|
102
|
+
type: :development
|
103
|
+
prerelease: false
|
104
|
+
version_requirements: !ruby/object:Gem::Requirement
|
105
|
+
none: false
|
106
|
+
requirements:
|
107
|
+
- - ! '>='
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0'
|
110
|
+
- !ruby/object:Gem::Dependency
|
111
|
+
name: cucumber
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
113
|
+
none: false
|
114
|
+
requirements:
|
115
|
+
- - ! '>='
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
none: false
|
122
|
+
requirements:
|
123
|
+
- - ! '>='
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '0'
|
126
|
+
- !ruby/object:Gem::Dependency
|
127
|
+
name: rake
|
128
|
+
requirement: !ruby/object:Gem::Requirement
|
129
|
+
none: false
|
130
|
+
requirements:
|
131
|
+
- - ! '>='
|
132
|
+
- !ruby/object:Gem::Version
|
133
|
+
version: '0'
|
134
|
+
type: :development
|
135
|
+
prerelease: false
|
136
|
+
version_requirements: !ruby/object:Gem::Requirement
|
137
|
+
none: false
|
138
|
+
requirements:
|
139
|
+
- - ! '>='
|
140
|
+
- !ruby/object:Gem::Version
|
141
|
+
version: '0'
|
142
|
+
- !ruby/object:Gem::Dependency
|
143
|
+
name: debugger
|
144
|
+
requirement: !ruby/object:Gem::Requirement
|
145
|
+
none: false
|
146
|
+
requirements:
|
147
|
+
- - ! '>='
|
148
|
+
- !ruby/object:Gem::Version
|
149
|
+
version: '0'
|
150
|
+
type: :development
|
151
|
+
prerelease: false
|
152
|
+
version_requirements: !ruby/object:Gem::Requirement
|
153
|
+
none: false
|
154
|
+
requirements:
|
155
|
+
- - ! '>='
|
156
|
+
- !ruby/object:Gem::Version
|
157
|
+
version: '0'
|
158
|
+
description: ! '"Backup from AWS RDS snapshot to AWS S3 as mysqldump"'
|
159
|
+
email:
|
160
|
+
- tamouse@gmail.com
|
161
|
+
executables:
|
162
|
+
- rds-s3-backup.rb
|
163
|
+
extensions: []
|
164
|
+
extra_rdoc_files: []
|
165
|
+
files:
|
166
|
+
- .gitignore
|
167
|
+
- .rspec
|
168
|
+
- .rvmrc
|
169
|
+
- Gemfile
|
170
|
+
- LICENSE.txt
|
171
|
+
- README.md
|
172
|
+
- Rakefile
|
173
|
+
- bin/rds-s3-backup.rb
|
174
|
+
- lib/rds-s3-backup.rb
|
175
|
+
- lib/rds-s3-backup/datadog.rb
|
176
|
+
- lib/rds-s3-backup/myrds.rb
|
177
|
+
- lib/rds-s3-backup/mys3.rb
|
178
|
+
- lib/rds-s3-backup/mysqlcmds.rb
|
179
|
+
- lib/rds-s3-backup/version.rb
|
180
|
+
- rds-s3-backup.gemspec
|
181
|
+
- spec/rds-s3-backup_spec.rb
|
182
|
+
- spec/spec_helper.rb
|
183
|
+
- test_data/.gitignore
|
184
|
+
- test_data/sample-config.yml
|
185
|
+
- test_output/.gitkeep
|
186
|
+
homepage: ''
|
187
|
+
licenses: []
|
188
|
+
post_install_message:
|
189
|
+
rdoc_options: []
|
190
|
+
require_paths:
|
191
|
+
- lib
|
192
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
193
|
+
none: false
|
194
|
+
requirements:
|
195
|
+
- - ! '>='
|
196
|
+
- !ruby/object:Gem::Version
|
197
|
+
version: '0'
|
198
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
199
|
+
none: false
|
200
|
+
requirements:
|
201
|
+
- - ! '>='
|
202
|
+
- !ruby/object:Gem::Version
|
203
|
+
version: '0'
|
204
|
+
requirements: []
|
205
|
+
rubyforge_project:
|
206
|
+
rubygems_version: 1.8.24
|
207
|
+
signing_key:
|
208
|
+
specification_version: 3
|
209
|
+
summary: ! '"Backup from AWS RDS snapshot to AWS S3 as mysqldump"'
|
210
|
+
test_files:
|
211
|
+
- spec/rds-s3-backup_spec.rb
|
212
|
+
- spec/spec_helper.rb
|