rds-s3-backup 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
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
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in rds-s3-backup.gemspec
4
+ gemspec
@@ -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.
@@ -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
@@ -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,7 @@
1
+ module Rds
2
+ module S3
3
+ module Backup
4
+ VERSION = "0.0.1"
5
+ end
6
+ end
7
+ 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
@@ -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,2 @@
1
+ obfuscate.sql
2
+ config.yml
@@ -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