rds_backup_service 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +12 -0
- data/.yardopts +1 -0
- data/API.md +29 -0
- data/Gemfile +2 -0
- data/LICENSE.TXT +10 -0
- data/README.md +125 -0
- data/Rakefile +18 -0
- data/config/accounts.example.yml +46 -0
- data/config/settings.yml +26 -0
- data/config.ru +2 -0
- data/lib/rds_backup_service/config.rb +68 -0
- data/lib/rds_backup_service/model/backup_job.rb +252 -0
- data/lib/rds_backup_service/model/delayed_job.rb +10 -0
- data/lib/rds_backup_service/model/email.rb +42 -0
- data/lib/rds_backup_service/rds_backup_service.rb +72 -0
- data/lib/rds_backup_service/service.rb +59 -0
- data/lib/rds_backup_service/tasks.rb +2 -0
- data/lib/rds_backup_service/version.rb +7 -0
- data/lib/rds_backup_service.rb +8 -0
- data/lib/tasks/setup_groups.rake +6 -0
- data/rds_backup_service.gemspec +41 -0
- metadata +234 -0
data/.gitignore
ADDED
data/.yardopts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--no-private --protected lib/**/*.rb - API.md
|
data/API.md
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
RDS Backup Service REST API
|
2
|
+
================
|
3
|
+
The REST API exposes only one API call:
|
4
|
+
|
5
|
+
POST /api/v1/backups
|
6
|
+
|
7
|
+
POST to here with parameter `rds_instance` set to an RDS instance name.
|
8
|
+
|
9
|
+
----------------
|
10
|
+
Backups
|
11
|
+
----------------
|
12
|
+
A Backup represents a long-running backup process for an RDS.
|
13
|
+
|
14
|
+
**RESOURCE LOCATION**: `/api/v1/backups`
|
15
|
+
|
16
|
+
**REQUEST PARAMETERS**:
|
17
|
+
|
18
|
+
* rds_instance: the name of the RDS instance to dump to S3 - **REQUIRED**
|
19
|
+
* email: an optional email address to send a message to on completion
|
20
|
+
|
21
|
+
|
22
|
+
**RESULT FIELDS**:
|
23
|
+
|
24
|
+
* rds_instance: the name of the RDS instance being dumped to S3
|
25
|
+
* account_name: the name of account from accounts.yml, if determined
|
26
|
+
* backup_status: an HTTP-like status code for the process
|
27
|
+
* status_url: a signed S3 URL to this job's JSON status as updated
|
28
|
+
* status_message: a descriptive progress message
|
29
|
+
* files: an Array of output files for this job, including S3 URLs
|
data/Gemfile
ADDED
data/LICENSE.TXT
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
----------------
|
2
|
+
*MIT License*
|
3
|
+
|
4
|
+
Copyright (c) 2012 Benton Roberts
|
5
|
+
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
7
|
+
|
8
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
9
|
+
|
10
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,125 @@
|
|
1
|
+
RDS Backup Service
|
2
|
+
================
|
3
|
+
Fire-and-forget SQL backups of Amazon Web Services' RDS databases into S3.
|
4
|
+
|
5
|
+
----------------
|
6
|
+
What is it?
|
7
|
+
----------------
|
8
|
+
A REST-style web service and middleware library for safely dumping the contents
|
9
|
+
of a live AWS Relational Database Service instance into a compressed SQL file.
|
10
|
+
|
11
|
+
The service has only one API call (a POST to `/api/v1/backups`), which spawns
|
12
|
+
a long-running worker process. The worker performs the following steps:
|
13
|
+
|
14
|
+
1. Snapshots the original RDS
|
15
|
+
2. Creates a new RDS instance based on the snapshot
|
16
|
+
3. Configures the new RDS as needed, including rebooting for Parameter Group
|
17
|
+
4. Connects to the RDS and dumps the database contents, compressing on the fly
|
18
|
+
5. Uploads the compressed SQL file to S3, and optionally emails its URL
|
19
|
+
6. Deletes up the snapshot, temporary instance, and local SQL dump
|
20
|
+
|
21
|
+
----------------
|
22
|
+
Why is it?
|
23
|
+
----------------
|
24
|
+
Safely and consistently grabbing the contents of a loaded, live RDS instance
|
25
|
+
is a pain (if it has no existing slave). Though the steps are simple, they're
|
26
|
+
brittle, slow, and involve lots of waiting for indeterminate time periods.
|
27
|
+
|
28
|
+
----------------
|
29
|
+
Installation
|
30
|
+
----------------
|
31
|
+
First install the dependencies:
|
32
|
+
|
33
|
+
* Ruby 1.9, rake, and bundler
|
34
|
+
* [Redis][] (for [Resque][] workers), or [DelayedJob][] (library only for now)
|
35
|
+
|
36
|
+
The RDS Backup Service can be installed as a standalone application or as a
|
37
|
+
Rack middleware library.
|
38
|
+
|
39
|
+
### To install as an application ###
|
40
|
+
|
41
|
+
Install project dependencies, fetch the code, and bundle up.
|
42
|
+
|
43
|
+
gem install rake bundler
|
44
|
+
git clone https://github.com/benton/rds_backup_service.git
|
45
|
+
cd rds_backup_service
|
46
|
+
bundle
|
47
|
+
|
48
|
+
### To install as a library ###
|
49
|
+
|
50
|
+
1) Install the gem, or add it as a Bundler dependency and `bundle`.
|
51
|
+
|
52
|
+
gem install rds_backup_service
|
53
|
+
|
54
|
+
2) Require the middleware from your Rack application, then insert it
|
55
|
+
in the stack:
|
56
|
+
|
57
|
+
require 'rds_backup_service'
|
58
|
+
...
|
59
|
+
config.middleware.use RDSBackup::Service # (Rails application.rb)
|
60
|
+
# or
|
61
|
+
use RDSBackup::Service # (Sinatra)
|
62
|
+
|
63
|
+
3) If desired, require the SecurityGroup setup task in your `Rakefile`:
|
64
|
+
|
65
|
+
require 'rds_backup_service/tasks'
|
66
|
+
|
67
|
+
----------------
|
68
|
+
Configuration and Setup
|
69
|
+
----------------
|
70
|
+
Two configuration files are required _(see included examples)_:
|
71
|
+
|
72
|
+
* `./config/accounts.yml` or `ENV['RDS_ACCOUNTS_FILE']`
|
73
|
+
|
74
|
+
This file defines three different types of AWS accounts: the various RDS
|
75
|
+
accounts to grab SQL from; the S3 account where the SQL output
|
76
|
+
will be written; and an optional EC2 account, which is used by the
|
77
|
+
`setup:rds_backup_groups` rake task to perform post-configuration setup.
|
78
|
+
|
79
|
+
* `./config/settings.yml` or `ENV['RDS_SETTINGS_FILE']`
|
80
|
+
|
81
|
+
This file defines the S3 bucket name for the output, plus some other options.
|
82
|
+
|
83
|
+
Once these files have been edited, run `rake setup:rds_backup_groups`, which:
|
84
|
+
|
85
|
+
* makes sure the configured Security Groups exist in all the RDS and EC2 accounts
|
86
|
+
* opens the RDS Security Group in each RDS account to the EC2 Security Group
|
87
|
+
* checks to see that the current host is in the EC2 Security Group (when in EC2)
|
88
|
+
|
89
|
+
----------------
|
90
|
+
Usage
|
91
|
+
----------------
|
92
|
+
The service is run in the standard Rack manner:
|
93
|
+
|
94
|
+
bundle exec rackup
|
95
|
+
|
96
|
+
The entry point for the REST API is `/api/v1/backups`
|
97
|
+
(See the {file:API.md API documentation})
|
98
|
+
|
99
|
+
The Resque workers are run with:
|
100
|
+
|
101
|
+
QUEUE=backups rake resque:work
|
102
|
+
|
103
|
+
|
104
|
+
----------------
|
105
|
+
DelayedJob
|
106
|
+
----------------
|
107
|
+
The library (though not the service) can be used with DelayedJob.
|
108
|
+
Place some code like this in your Controller or Model:
|
109
|
+
|
110
|
+
require 'rds_backup_service'
|
111
|
+
...
|
112
|
+
job = RDSBackup::Job.new(params[:rds_id])
|
113
|
+
job.write_to_s3
|
114
|
+
Delayed::Job.enqueue RDSBackup::DelayedJob.new(job.rds_id, {
|
115
|
+
'backup_id' => job.backup_id,
|
116
|
+
'requested' => job.requested.to_s,
|
117
|
+
'email' => params[:email],
|
118
|
+
})
|
119
|
+
|
120
|
+
|
121
|
+
|
122
|
+
[Redis]: http://redis.io/
|
123
|
+
[Resque]: https://github.com/defunkt/resque
|
124
|
+
[DelayedJob]: https://github.com/collectiveidea/delayed_job
|
125
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# Set up bundler
|
2
|
+
%w{rubygems bundler bundler/gem_tasks}.each {|dep| require dep}
|
3
|
+
bundles = [:default]
|
4
|
+
Bundler.setup(:default)
|
5
|
+
case ENV['RACK_ENV']
|
6
|
+
when 'development' then Bundler.setup(:default, :development)
|
7
|
+
when 'test' then Bundler.setup(:default, :development, :test)
|
8
|
+
end
|
9
|
+
|
10
|
+
require 'rds_backup_service'
|
11
|
+
require 'resque/tasks'
|
12
|
+
ENV['TERM_CHILD'] = '1'
|
13
|
+
|
14
|
+
# Load all tasks from 'lib/tasks'
|
15
|
+
Dir["#{File.dirname(__FILE__)}/lib/tasks/*.rake"].sort.each {|ext| load ext}
|
16
|
+
|
17
|
+
desc 'Default: runs all tests'
|
18
|
+
task :default => :spec
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# The S3 account is used to store all job output.
|
2
|
+
s3account-name:
|
3
|
+
credentials:
|
4
|
+
aws_access_key_id: XXXXXXXXXXXXXXXXXXXX
|
5
|
+
aws_secret_access_key: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
|
6
|
+
provider: AWS
|
7
|
+
service: Storage
|
8
|
+
|
9
|
+
# This service should run in an EC2 account.
|
10
|
+
# The creds here are used only for automated SecurityGroup setup.
|
11
|
+
# If you don't run 'rake setup:rds_backup_groups',
|
12
|
+
# then these won't be used -- but one-time EC2 setup must be done manually.
|
13
|
+
ec2-account-for-service:
|
14
|
+
credentials:
|
15
|
+
aws_access_key_id: XXXXXXXXXXXXXXXXXXXX
|
16
|
+
aws_secret_access_key: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
|
17
|
+
provider: AWS
|
18
|
+
service: Compute
|
19
|
+
|
20
|
+
# The RDS instances in all the RDS accounts are avaliable for backup.
|
21
|
+
|
22
|
+
rds-development:
|
23
|
+
credentials:
|
24
|
+
aws_access_key_id: XXXXXXXXXXXXXXXXXXXX
|
25
|
+
aws_secret_access_key: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
|
26
|
+
delay: 120
|
27
|
+
provider: AWS
|
28
|
+
service: RDS
|
29
|
+
exclude_resources:
|
30
|
+
- parameters
|
31
|
+
- security_groups
|
32
|
+
- parameter_groups
|
33
|
+
- snapshots
|
34
|
+
|
35
|
+
rds-production:
|
36
|
+
credentials:
|
37
|
+
aws_access_key_id: XXXXXXXXXXXXXXXXXXXX
|
38
|
+
aws_secret_access_key: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
|
39
|
+
delay: 120
|
40
|
+
provider: AWS
|
41
|
+
service: RDS
|
42
|
+
exclude_resources:
|
43
|
+
- parameters
|
44
|
+
- security_groups
|
45
|
+
- parameter_groups
|
46
|
+
- snapshots
|
data/config/settings.yml
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# REQUIRED - The S3 bucket name where all backups will be saved
|
2
|
+
backup_bucket: rdsbackups
|
3
|
+
|
4
|
+
# Backups can be prefixed with backup_prefix if desired.
|
5
|
+
# (Do not include a leading or trailing /)
|
6
|
+
# default value = nil
|
7
|
+
|
8
|
+
#backup_prefix: testing
|
9
|
+
|
10
|
+
# The rds_security_group must be defined in each
|
11
|
+
# RDS account, and be open to this server, so that the
|
12
|
+
# mysqldump utlility can connect.
|
13
|
+
# default value = rds-backup-service
|
14
|
+
|
15
|
+
#rds_security_group: rds-backup-service
|
16
|
+
|
17
|
+
# The ec2_security_group must be defined in the EC2 account
|
18
|
+
# under which this service runs.
|
19
|
+
# default value = rds-backup-service
|
20
|
+
|
21
|
+
#ec2_security_group: rds-backup-service
|
22
|
+
|
23
|
+
# Where to store DB data while compressing it, before upload
|
24
|
+
# defaults to Ruby's Dir.tmpdir
|
25
|
+
|
26
|
+
#tmp_dir: "/tmp"
|
data/config.ru
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
module RDSBackup
|
2
|
+
# models logic for post-configuration setup
|
3
|
+
module Config
|
4
|
+
|
5
|
+
# Attempts to set up the EC2 and RDS security groups as specified in the
|
6
|
+
# configuration. Raises an Exception on errors. Best if run from EC2.
|
7
|
+
def self.setup_security_groups(logger = nil)
|
8
|
+
log = logger || RDSBackup.default_logger(STDOUT)
|
9
|
+
# Configuration
|
10
|
+
log.info "Scanning system..."
|
11
|
+
(system = Ohai::System.new).all_plugins
|
12
|
+
log.info "Reading config files..."
|
13
|
+
settings = RDSBackup.settings
|
14
|
+
ec2_group_name = settings['ec2_security_group']
|
15
|
+
rds_group_name = settings['rds_security_group']
|
16
|
+
ec2 = RDSBackup.ec2
|
17
|
+
|
18
|
+
# EC2 Security Group creation
|
19
|
+
log.info "Checking EC2 for Security Group #{ec2_group_name}"
|
20
|
+
unless ec2_group = ec2.security_groups.get(ec2_group_name)
|
21
|
+
log.info "Creating EC2 Security group #{ec2_group_name}"
|
22
|
+
ec2_group = ec2.security_groups.create(:name => ec2_group_name,
|
23
|
+
:description => 'Created by rds_backup_service')
|
24
|
+
end
|
25
|
+
|
26
|
+
# RDS Security Group creation and authorization
|
27
|
+
RDSBackup.rds_accounts.each do |account_name, account_data|
|
28
|
+
log.info "Checking account #{account_name} for "+
|
29
|
+
"RDS Security group #{rds_group_name}"
|
30
|
+
rds = ::Fog::AWS::RDS.new(account_data[:credentials])
|
31
|
+
rds_group = rds.security_groups.get rds_group_name
|
32
|
+
unless rds_group
|
33
|
+
log.info "Creating security group #{rds_group_name} in #{account_name}"
|
34
|
+
rds_group = rds.security_groups.create(:id => rds_group_name,
|
35
|
+
:description => 'Created by rds_backup_service')
|
36
|
+
end
|
37
|
+
# Apply EC2 authorization to RDS Security Groups
|
38
|
+
owner = ec2.security_groups.first.owner_id
|
39
|
+
authorized = false
|
40
|
+
rds_group.ec2_security_groups.each do |authorization|
|
41
|
+
if (authorization['EC2SecurityGroupName'] == ec2_group_name) &&
|
42
|
+
(authorization['EC2SecurityGroupOwnerId'] == owner)
|
43
|
+
authorized = true
|
44
|
+
end
|
45
|
+
end
|
46
|
+
unless authorized
|
47
|
+
log.info "Authorizing EC2 Group for #{account_name}/#{rds_group_name}"
|
48
|
+
rds_group.authorize_ec2_security_group(ec2_group_name, owner)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# EC2 Security Group check for this host
|
53
|
+
unless system[:ec2]
|
54
|
+
log.warn "Not running in EC2 - open RDS groups to this host!"
|
55
|
+
else
|
56
|
+
unless this_host = ec2.servers.get(system[:ec2][:instance_id])
|
57
|
+
log.warn "Not running in EC2 account #{s3_acc_name}!"
|
58
|
+
else
|
59
|
+
log.info "Running in EC2. Current Security Groups = #{this_host.groups}"
|
60
|
+
unless this_host.groups.include? ec2_group_name
|
61
|
+
log.warn "This host is not in Security Group #{ec2_group_name}!"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,252 @@
|
|
1
|
+
require 'fog_tracker'
|
2
|
+
require 'fileutils'
|
3
|
+
require 'json'
|
4
|
+
module RDSBackup
|
5
|
+
# Backs up the contents of a single RDS database to S3
|
6
|
+
class Job
|
7
|
+
|
8
|
+
@queue = :backups
|
9
|
+
|
10
|
+
attr_reader :backup_id, :rds_id, :account_name, :options
|
11
|
+
attr_reader :status, :status_url, :message, :files, :requested
|
12
|
+
|
13
|
+
# Constructor.
|
14
|
+
# @param [String] rds_instance_id the ID of the RDS instance to backup
|
15
|
+
# @param [Hash] options optional additional parameters:
|
16
|
+
# - backup_id - a unique ID for this job, if necessary
|
17
|
+
# - requested - a Time when this job was requested
|
18
|
+
# - email - an email address to be notified on completion
|
19
|
+
# - logger - a Logger object, for printing this job's ongoing status
|
20
|
+
def initialize(rds_instance_id, options = {})
|
21
|
+
@rds_id, @options = rds_instance_id, options
|
22
|
+
@backup_id = options['backup_id'] || "%016x" % (rand * 0xffffffffffffffff)
|
23
|
+
@requested = options['requested'] ? Time.parse(options['requested']) : Time.now
|
24
|
+
@status = 200
|
25
|
+
@message = "queued"
|
26
|
+
@files = []
|
27
|
+
@config = RDSBackup.settings
|
28
|
+
@bucket = @config['backup_bucket']
|
29
|
+
@s3_path = (@config['backup_prefix'] ? "#{@config['backup_prefix']}/" : "")+
|
30
|
+
"#{requested.strftime("%Y/%m/%d")}/#{rds_id}/#{backup_id}"
|
31
|
+
@snapshot_id = "rds-backup-service-#{rds_id}-#{backup_id}"
|
32
|
+
@new_rds_id = "rds-backup-service-#{backup_id}"
|
33
|
+
@new_password = "#{backup_id}"
|
34
|
+
@account_name = options['account_name']
|
35
|
+
end
|
36
|
+
|
37
|
+
# returns a JSON-format String representation of this backup job
|
38
|
+
def to_json
|
39
|
+
JSON.pretty_generate({
|
40
|
+
rds_instance: rds_id,
|
41
|
+
account_name: account_name,
|
42
|
+
backup_status: status,
|
43
|
+
status_message: message,
|
44
|
+
status_url: status_url,
|
45
|
+
files: files,
|
46
|
+
})
|
47
|
+
end
|
48
|
+
|
49
|
+
# Writes this job's JSON representation to S3
|
50
|
+
def write_to_s3
|
51
|
+
status_path = "#{@s3_path}/status.json"
|
52
|
+
s3.put_object(@bucket, status_path, "#{to_json}\n")
|
53
|
+
unless @status_url
|
54
|
+
expire_date = Time.now + (3600 * 24) # one day from now
|
55
|
+
@status_url = s3.get_object_http_url(@bucket, status_path, expire_date)
|
56
|
+
s3.put_object(@bucket, status_path, "#{to_json}\n")
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Entry point for the Resque framework.
|
61
|
+
# Parameters are the same as for #initialize()
|
62
|
+
def self.perform(rds_instance_id, options = {})
|
63
|
+
job = Job.new(rds_instance_id, options)
|
64
|
+
begin
|
65
|
+
job.perform_backup
|
66
|
+
rescue Resque::TermException => e
|
67
|
+
::Resque.enqueue_to(:backups, Job, rds_instance_id,
|
68
|
+
options.merge(backup_id: job.backup_id, requested: job.requested.to_s))
|
69
|
+
job.update_status "Terminated on interrupt signal - requeued"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# Top-level, long-running method for performing the backup.
|
74
|
+
def perform_backup
|
75
|
+
begin
|
76
|
+
prepare_backup
|
77
|
+
update_status "Backing up #{rds_id} from account #{account_name}"
|
78
|
+
create_disconnected_rds
|
79
|
+
download_data_from_tmp_rds # populates @sql_file
|
80
|
+
delete_disconnected_rds
|
81
|
+
upload_output_to_s3
|
82
|
+
update_status "Backup of #{rds_id} complete"
|
83
|
+
send_mail
|
84
|
+
rescue Exception => e
|
85
|
+
update_status "ERROR: #{e.message.split("\n").first}", 500
|
86
|
+
raise e
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# Step 1 of the overall process - create a disconnected copy of the RDS
|
91
|
+
def create_disconnected_rds(new_rds_name = nil)
|
92
|
+
@new_rds_id = new_rds_name if new_rds_name
|
93
|
+
prepare_backup unless @original_server # in case run as a convenience method
|
94
|
+
snapshot_original_rds
|
95
|
+
create_tmp_rds_from_snapshot
|
96
|
+
configure_tmp_rds
|
97
|
+
wait_for_new_security_group
|
98
|
+
wait_for_new_parameter_group # (reboots as needed)
|
99
|
+
destroy_snapshot
|
100
|
+
end
|
101
|
+
|
102
|
+
# Queries RDS for any pre-existing entities associated with this job.
|
103
|
+
# Also waits for the original RDS to become ready.
|
104
|
+
def prepare_backup
|
105
|
+
unless @original_server = RDSBackup.get_rds(rds_id)
|
106
|
+
names = RDSBackup.rds_accounts.map {|name, account| name }
|
107
|
+
raise "Unable to find RDS #{rds_id} in accounts #{names.join ", "}"
|
108
|
+
end
|
109
|
+
@account_name = @original_server.tracker_account[:name]
|
110
|
+
@rds = ::Fog::AWS::RDS.new(
|
111
|
+
RDSBackup.rds_accounts[@account_name][:credentials])
|
112
|
+
@snapshot = @rds.snapshots.get @snapshot_id
|
113
|
+
@new_instance = @rds.servers.get @new_rds_id
|
114
|
+
end
|
115
|
+
|
116
|
+
# Snapshots the original RDS
|
117
|
+
def snapshot_original_rds
|
118
|
+
unless @new_instance || @snapshot
|
119
|
+
update_status "Waiting for RDS instance #{@original_server.id}"
|
120
|
+
@original_server.wait_for { ready? }
|
121
|
+
update_status "Creating snapshot #{@snapshot_id} from RDS #{rds_id}"
|
122
|
+
@snapshot = @rds.snapshots.create(id: @snapshot_id, instance_id: rds_id)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# Creates a new RDS from the snapshot
|
127
|
+
def create_tmp_rds_from_snapshot
|
128
|
+
unless @new_instance
|
129
|
+
update_status "Waiting for snapshot #{@snapshot_id}"
|
130
|
+
@snapshot.wait_for { ready? }
|
131
|
+
update_status "Booting new RDS #{@new_rds_id} from snapshot #{@snapshot.id}"
|
132
|
+
@rds.restore_db_instance_from_db_snapshot(@snapshot.id,
|
133
|
+
@new_rds_id, 'DBInstanceClass' => @original_server.flavor_id)
|
134
|
+
@new_instance = @rds.servers.get @new_rds_id
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
# Destroys the snapshot if it exists
|
139
|
+
def destroy_snapshot
|
140
|
+
if (@snapshot = @rds.snapshots.get @snapshot_id)
|
141
|
+
update_status "Deleting snapshot #{@snapshot.id}"
|
142
|
+
@snapshot.destroy
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
# Updates the Master Password and applies the configured RDS Security Group
|
147
|
+
def configure_tmp_rds
|
148
|
+
update_status "Waiting for instance #{@new_instance.id}..."
|
149
|
+
@new_instance.wait_for { ready? }
|
150
|
+
update_status "Modifying RDS attributes for new RDS #{@new_instance.id}"
|
151
|
+
@rds.modify_db_instance(@new_instance.id, true, {
|
152
|
+
'DBParameterGroupName' => @original_server.db_parameter_groups.
|
153
|
+
first['DBParameterGroupName'],
|
154
|
+
'DBSecurityGroups' => [ @config['rds_security_group'] ],
|
155
|
+
'MasterUserPassword' => @new_password,
|
156
|
+
})
|
157
|
+
end
|
158
|
+
|
159
|
+
# Wait for the new RDS Security Group to become 'active'
|
160
|
+
def wait_for_new_security_group
|
161
|
+
old_group_name = @config['rds_security_group']
|
162
|
+
update_status "Applying security group #{old_group_name}"+
|
163
|
+
" to #{@new_instance.id}"
|
164
|
+
@new_instance.wait_for {
|
165
|
+
new_group = (db_security_groups.select do |group|
|
166
|
+
group['DBSecurityGroupName'] == old_group_name
|
167
|
+
end).first
|
168
|
+
(new_group ? new_group['Status'] : 'Unknown') == 'active'
|
169
|
+
}
|
170
|
+
end
|
171
|
+
|
172
|
+
# Wait for the new RDS Parameter Group to become 'in-sync'
|
173
|
+
def wait_for_new_parameter_group
|
174
|
+
old_name = @original_server.db_parameter_groups.first['DBParameterGroupName']
|
175
|
+
update_status "Applying parameter group #{old_name} to #{@new_instance.id}"
|
176
|
+
job = self # save local var for closure in wait_for, below
|
177
|
+
@new_instance.wait_for {
|
178
|
+
new_group = (db_parameter_groups.select do |group|
|
179
|
+
group['DBParameterGroupName'] == old_name
|
180
|
+
end).first
|
181
|
+
status = (new_group ? new_group['ParameterApplyStatus'] : 'Unknown')
|
182
|
+
if (status == "pending-reboot")
|
183
|
+
job.update_status "Rebooting RDS #{id} to apply ParameterGroup #{old_name}"
|
184
|
+
reboot and wait_for { ready? }
|
185
|
+
end
|
186
|
+
status == 'in-sync' && ready?
|
187
|
+
}
|
188
|
+
end
|
189
|
+
|
190
|
+
# Connects to the RDS server, and dumps the database to a temp dir
|
191
|
+
def download_data_from_tmp_rds
|
192
|
+
@new_instance.wait_for { ready? }
|
193
|
+
db_name = @original_server.db_name
|
194
|
+
db_user = @original_server.master_username
|
195
|
+
update_status "Dumping database #{db_name} from #{@new_instance.id}"
|
196
|
+
dump_time = @snapshot ? Time.parse(@snapshot.created_at.to_s) : Time.now
|
197
|
+
date_stamp = dump_time.strftime("%Y-%m-%d-%H%M%S")
|
198
|
+
@sql_file = "#{@config['tmp_dir']}/#{@s3_path}/#{db_name}.#{date_stamp}.sql.gz"
|
199
|
+
hostname = @new_instance.endpoint['Address']
|
200
|
+
dump_cmd = "mysqldump -u #{db_user} -h #{hostname} "+
|
201
|
+
"-p#{@new_password} #{db_name} | gzip >#{@sql_file}"
|
202
|
+
FileUtils.mkpath(File.dirname @sql_file)
|
203
|
+
@log.debug "Executing command: #{dump_cmd}"
|
204
|
+
`#{dump_cmd}`
|
205
|
+
end
|
206
|
+
|
207
|
+
# Destroys the temporary RDS instance
|
208
|
+
def delete_disconnected_rds
|
209
|
+
update_status "Deleting RDS instance #{@new_instance.id}"
|
210
|
+
@new_instance.destroy
|
211
|
+
end
|
212
|
+
|
213
|
+
# Uploads the compressed SQL file to S3
|
214
|
+
def upload_output_to_s3
|
215
|
+
update_status "Uploading output file #{::File.basename @sql_file}"
|
216
|
+
dump_path = "#{@s3_path}/#{::File.basename @sql_file}"
|
217
|
+
s3.put_object(@bucket, dump_path, File.read(@sql_file))
|
218
|
+
upload = s3.directories.get(@bucket).files.get dump_path
|
219
|
+
@files = [ {
|
220
|
+
name: ::File.basename(@sql_file),
|
221
|
+
size: upload.content_length,
|
222
|
+
url: upload.url(Time.now + (3600 * 24 * 30)) # 30 days from now
|
223
|
+
} ]
|
224
|
+
@log.info "Deleting tmp directory #{File.dirname @sql_file}"
|
225
|
+
FileUtils.rm_rf(File.dirname @sql_file)
|
226
|
+
end
|
227
|
+
|
228
|
+
# Writes a new status message to the log, and writes the job info to S3
|
229
|
+
def update_status(message, new_status = nil)
|
230
|
+
@log = @options['logger'] || RDSBackup.default_logger(STDOUT)
|
231
|
+
@message = message
|
232
|
+
@status = new_status if new_status
|
233
|
+
@status == 200 ? (@log.info message) : (@log.error message)
|
234
|
+
write_to_s3
|
235
|
+
end
|
236
|
+
|
237
|
+
# Sends a status email
|
238
|
+
def send_mail
|
239
|
+
return unless @options['email']
|
240
|
+
@log.info "Emailing #{@options['email']}..."
|
241
|
+
begin
|
242
|
+
RDSBackup::Email.new(self).send!
|
243
|
+
rescue Exception => e
|
244
|
+
@log.warn "Error sending email: #{e.message.split("\n").first}"
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
# lazily initializes and returns S3 connection
|
249
|
+
def s3 ; @s3 ||= RDSBackup.s3 end
|
250
|
+
|
251
|
+
end
|
252
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
module RDSBackup
|
2
|
+
# convenience wrapper class for DelayedJob.
|
3
|
+
# Parameters are the same as for RDSBackup::Job.initialize()
|
4
|
+
class DelayedJob < Struct.new(:rds_id, :options)
|
5
|
+
# Entry point for the DelayedJob framework.
|
6
|
+
def perform
|
7
|
+
RDSBackup::Job.new(rds_id, options).perform_backup
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'mail'
|
2
|
+
module RDSBackup
|
3
|
+
# an email representation of a Job, that can send itself to recipients.
|
4
|
+
class Email
|
5
|
+
|
6
|
+
attr_reader :job
|
7
|
+
|
8
|
+
# constructor - requires an RDSBackup::Job
|
9
|
+
def initialize(backup_job)
|
10
|
+
@job = backup_job
|
11
|
+
end
|
12
|
+
|
13
|
+
# Attempts to send email through local ESMTP port 25.
|
14
|
+
# Raises an Exception on failure.
|
15
|
+
def send!
|
16
|
+
raise "job #{job.backup_id} has no email option" unless job.options['email']
|
17
|
+
main_text = body_text # define local variables for closure over Mail.new
|
18
|
+
recipients, header = job.options['email'],
|
19
|
+
header = "Backup of RDS #{job.rds_id} (job ID #{job.backup_id})"
|
20
|
+
mail = Mail.new do
|
21
|
+
from 'rdsbackupservice@mdsol.com'
|
22
|
+
to recipients
|
23
|
+
subject header
|
24
|
+
body "#{main_text}\n"
|
25
|
+
end
|
26
|
+
mail.deliver!
|
27
|
+
end
|
28
|
+
|
29
|
+
# defines the body of a Job's status email
|
30
|
+
def body_text
|
31
|
+
msg = "Hello.\n\n"
|
32
|
+
if job.status == 200
|
33
|
+
msg += "Your backup of database #{job.rds_id} is complete.\n"+
|
34
|
+
(job.files.empty? ? "" : "Output is at #{job.files.first[:url]}\n")
|
35
|
+
else
|
36
|
+
msg += "Your backup is incomplete. (job ID #{job.backup_id})\n"
|
37
|
+
end
|
38
|
+
msg += "Job status: #{job.message}"
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# The top-level project module. Contains some static helper methods.
|
2
|
+
module RDSBackup
|
3
|
+
require 'fog'
|
4
|
+
require 'tmpdir'
|
5
|
+
require 'ohai'
|
6
|
+
PROJECT_DIR = File.expand_path(File.join(File.dirname(__FILE__), '..', '..'))
|
7
|
+
require "#{PROJECT_DIR}/lib/rds_backup_service/version"
|
8
|
+
|
9
|
+
# Loads account information defined in config/accounts.yml, or
|
10
|
+
# ENV['RDS_ACCOUNTS_FILE'].
|
11
|
+
# @param account_file the path to a YAML file (see accounts.yml.example).
|
12
|
+
# @return [Hash] a Hash representing the account info.
|
13
|
+
# The keys in each account Hash are converted to Symbols.
|
14
|
+
def self.read_accounts(account_file = ENV['RDS_ACCOUNTS_FILE'])
|
15
|
+
YAML::load(File.read(account_file || "./config/accounts.yml")).
|
16
|
+
inject({}) {|a,b| a[b[0]] = b[1].symbolize_keys ; a }
|
17
|
+
end
|
18
|
+
|
19
|
+
# Loads account information, and returns only those
|
20
|
+
# entries that repesent RDS accounts.
|
21
|
+
def self.rds_accounts
|
22
|
+
RDSBackup.read_accounts.select{|id,acc| acc[:service] == 'RDS'}
|
23
|
+
end
|
24
|
+
|
25
|
+
# Returns a new connection to the AWS EC2 service (Fog::Compute::AWS)
|
26
|
+
def self.ec2
|
27
|
+
accts = RDSBackup.read_accounts.select{|id,acc| acc[:service] == 'Compute'}
|
28
|
+
raise "At least one S3 account must be defined" if accts.empty?
|
29
|
+
Fog::Compute::AWS.new(accts.first[1][:credentials])
|
30
|
+
end
|
31
|
+
|
32
|
+
# Returns a new connection to the AWS S3 service (Fog::Storage::AWS)
|
33
|
+
def self.s3
|
34
|
+
accts = RDSBackup.read_accounts.select{|id,acc| acc[:service] == 'Storage'}
|
35
|
+
raise "At least one S3 account must be defined" if accts.empty?
|
36
|
+
Fog::Storage::AWS.new(accts.first[1][:credentials])
|
37
|
+
end
|
38
|
+
|
39
|
+
# Returns the configuration Hash read from config/s3_account.yml
|
40
|
+
# or ENV['RDSDUMP_SETTINGS_FILE'].
|
41
|
+
def self.settings(settings_file = ENV['RDS_SETTINGS_FILE'])
|
42
|
+
{ # here are some defaults
|
43
|
+
'rds_security_group' => 'rds-backup-service',
|
44
|
+
'ec2_security_group' => 'rds-backup-service',
|
45
|
+
'tmp_dir' => Dir.tmpdir,
|
46
|
+
}.merge(YAML::load(
|
47
|
+
File.read(settings_file || "./config/settings.yml")))
|
48
|
+
end
|
49
|
+
|
50
|
+
# Defines the root URI path of the web service.
|
51
|
+
# @return [String] the root URI path of the web service
|
52
|
+
def self.root
|
53
|
+
"/api/v#{RDSBackup::API_VERSION}"
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.default_logger(output = nil)
|
57
|
+
logger = ::Logger.new(output)
|
58
|
+
logger.sev_threshold = Logger::INFO
|
59
|
+
logger.formatter = proc {|lvl, time, prog, msg| "#{lvl}: #{msg}\n"}
|
60
|
+
logger
|
61
|
+
end
|
62
|
+
|
63
|
+
# Returns a Fog RDS entity for a given an RDS ID. Polls all accounts.
|
64
|
+
# The account name is attached to the result as 'tracker_account[:name]'.
|
65
|
+
# @param [String] the name of the desired RDS entity
|
66
|
+
# @return [Fog::AWS::RDS::Server] the RDS instance, or nil if not found
|
67
|
+
def self.get_rds(rds_id)
|
68
|
+
::FogTracker::Tracker.new(RDSBackup.rds_accounts).update.
|
69
|
+
select{|rds| rds.identity == rds_id}.first
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require File.join(File.dirname(__FILE__), '..', 'rds_backup_service')
|
3
|
+
require 'sinatra/base'
|
4
|
+
require 'fog_tracker'
|
5
|
+
require 'resque'
|
6
|
+
|
7
|
+
module RDSBackup
|
8
|
+
# A RESTful web service for backing up RDS databases to S3.
|
9
|
+
# See the README for running this Rack-compliant service.
|
10
|
+
class Service < Sinatra::Base
|
11
|
+
|
12
|
+
# configure logging when not in test mode
|
13
|
+
configure :production, :development do
|
14
|
+
@log = RDSBackup.default_logger(STDOUT)
|
15
|
+
enable :logging
|
16
|
+
end
|
17
|
+
|
18
|
+
# on startup, load account information and start tracking RDS instances
|
19
|
+
configure do
|
20
|
+
@log.info "Loading account information..."
|
21
|
+
tracker = FogTracker::Tracker.new(RDSBackup.rds_accounts, :logger => @log)
|
22
|
+
@log.info "Starting tracker..."
|
23
|
+
tracker.update
|
24
|
+
tracker.start
|
25
|
+
set :tracker, tracker
|
26
|
+
end
|
27
|
+
|
28
|
+
before do ; content_type 'application/json' end # serve JSON
|
29
|
+
|
30
|
+
######## POST /api/vXXX/backups ########
|
31
|
+
# Queues a Job for a given :rds_instance
|
32
|
+
post "#{RDSBackup.root}/backups" do
|
33
|
+
rds_id = params[:rds_instance]
|
34
|
+
servers = settings.tracker['*::AWS::RDS::servers']
|
35
|
+
rds = (servers.select {|rds| rds.identity == rds_id}).first
|
36
|
+
|
37
|
+
# check for errors
|
38
|
+
if ! rds_id
|
39
|
+
return [ 400, { errors: ["Parameter 'rds_instance' required"]}.to_json ]
|
40
|
+
elsif ! (servers.map {|r| r.identity}).include?(rds_id)
|
41
|
+
return [ 404, { errors: ["RDS instance #{rds_id} not found"]}.to_json ]
|
42
|
+
end
|
43
|
+
|
44
|
+
# request is OK - queue up a BackupJob
|
45
|
+
job = Job.new(rds_id, params.merge(account_name: rds.tracker_account[:name]))
|
46
|
+
logger.info "Queuing backup of RDS #{rds_id} in account #{job.account_name}"
|
47
|
+
job.write_to_s3
|
48
|
+
::Resque.enqueue_to(:backups, Job, job.rds_id, job.options.
|
49
|
+
merge({'backup_id' => job.backup_id, 'requested' => job.requested.to_s}))
|
50
|
+
|
51
|
+
[ 201, # return HTTP_CREATED, and
|
52
|
+
{ 'Location' => job.status_url }, # point to the S3 document
|
53
|
+
"#{job.to_json}\n" # and return the job as JSON
|
54
|
+
]
|
55
|
+
end
|
56
|
+
|
57
|
+
run! if app_file == $0 # start the server if ruby file executed directly
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,8 @@
|
|
1
|
+
%w{rubygems bundler}.each {|lib| require lib}
|
2
|
+
Bundler.setup
|
3
|
+
|
4
|
+
# Load all the other files in this library, except the service
|
5
|
+
%w{ version rds_backup_service config
|
6
|
+
model/backup_job model/delayed_job model/email }.each do |file|
|
7
|
+
require File.join(File.dirname(__FILE__), "rds_backup_service/#{file}")
|
8
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "rds_backup_service/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "rds_backup_service"
|
7
|
+
s.version = RDSBackup::VERSION
|
8
|
+
s.authors = ["Benton Roberts"]
|
9
|
+
s.email = ["benton@bentonroberts.com"]
|
10
|
+
s.homepage = "http://github.com/benton/rds_backup_service"
|
11
|
+
s.summary = %q{Provides a REST API for backing up live RDS instances }+
|
12
|
+
%q{to S3 as a compressed SQL file.}
|
13
|
+
s.description = %q{Provides a REST API for backing up live RDS instances }+
|
14
|
+
%q{to S3 as a compressed SQL file.}
|
15
|
+
s.rubyforge_project = "rds_backup_service"
|
16
|
+
|
17
|
+
# This project is both a Gem and an Application,
|
18
|
+
# so the Gemfile.lock is included in the repo for application users,
|
19
|
+
# but excluded from the packaged Gem, for middleware use.
|
20
|
+
git_files = `git ls-files`.split("\n") # Read all files in the repo,
|
21
|
+
git_files.delete "Gemfile.lock" # remove Gemfile.lock, and
|
22
|
+
s.files = git_files # use the result in the Gem.
|
23
|
+
|
24
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
25
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
26
|
+
s.require_paths = ["lib"]
|
27
|
+
|
28
|
+
# Runtime dependencies
|
29
|
+
s.add_dependency "sinatra"
|
30
|
+
s.add_dependency "fog_tracker", ">=0.4.0"
|
31
|
+
s.add_dependency "resque"
|
32
|
+
s.add_dependency "mail"
|
33
|
+
s.add_dependency "ohai"
|
34
|
+
|
35
|
+
# Development / Test dependencies
|
36
|
+
s.add_development_dependency "rake"
|
37
|
+
s.add_development_dependency "rspec"
|
38
|
+
s.add_development_dependency "guard"
|
39
|
+
s.add_development_dependency "guard-rspec"
|
40
|
+
s.add_development_dependency "ruby_gntp"
|
41
|
+
end
|
metadata
ADDED
@@ -0,0 +1,234 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rds_backup_service
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Benton Roberts
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-09-04 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: sinatra
|
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_tracker
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: 0.4.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.4.0
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: resque
|
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: mail
|
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: ohai
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ! '>='
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
86
|
+
type: :runtime
|
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: rake
|
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: rspec
|
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: guard
|
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: guard-rspec
|
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
|
+
- !ruby/object:Gem::Dependency
|
159
|
+
name: ruby_gntp
|
160
|
+
requirement: !ruby/object:Gem::Requirement
|
161
|
+
none: false
|
162
|
+
requirements:
|
163
|
+
- - ! '>='
|
164
|
+
- !ruby/object:Gem::Version
|
165
|
+
version: '0'
|
166
|
+
type: :development
|
167
|
+
prerelease: false
|
168
|
+
version_requirements: !ruby/object:Gem::Requirement
|
169
|
+
none: false
|
170
|
+
requirements:
|
171
|
+
- - ! '>='
|
172
|
+
- !ruby/object:Gem::Version
|
173
|
+
version: '0'
|
174
|
+
description: Provides a REST API for backing up live RDS instances to S3 as a compressed
|
175
|
+
SQL file.
|
176
|
+
email:
|
177
|
+
- benton@bentonroberts.com
|
178
|
+
executables: []
|
179
|
+
extensions: []
|
180
|
+
extra_rdoc_files: []
|
181
|
+
files:
|
182
|
+
- .gitignore
|
183
|
+
- .yardopts
|
184
|
+
- API.md
|
185
|
+
- Gemfile
|
186
|
+
- LICENSE.TXT
|
187
|
+
- README.md
|
188
|
+
- Rakefile
|
189
|
+
- config.ru
|
190
|
+
- config/accounts.example.yml
|
191
|
+
- config/settings.yml
|
192
|
+
- lib/rds_backup_service.rb
|
193
|
+
- lib/rds_backup_service/config.rb
|
194
|
+
- lib/rds_backup_service/model/backup_job.rb
|
195
|
+
- lib/rds_backup_service/model/delayed_job.rb
|
196
|
+
- lib/rds_backup_service/model/email.rb
|
197
|
+
- lib/rds_backup_service/rds_backup_service.rb
|
198
|
+
- lib/rds_backup_service/service.rb
|
199
|
+
- lib/rds_backup_service/tasks.rb
|
200
|
+
- lib/rds_backup_service/version.rb
|
201
|
+
- lib/tasks/setup_groups.rake
|
202
|
+
- rds_backup_service.gemspec
|
203
|
+
homepage: http://github.com/benton/rds_backup_service
|
204
|
+
licenses: []
|
205
|
+
post_install_message:
|
206
|
+
rdoc_options: []
|
207
|
+
require_paths:
|
208
|
+
- lib
|
209
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
210
|
+
none: false
|
211
|
+
requirements:
|
212
|
+
- - ! '>='
|
213
|
+
- !ruby/object:Gem::Version
|
214
|
+
version: '0'
|
215
|
+
segments:
|
216
|
+
- 0
|
217
|
+
hash: 1151512606421308315
|
218
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
219
|
+
none: false
|
220
|
+
requirements:
|
221
|
+
- - ! '>='
|
222
|
+
- !ruby/object:Gem::Version
|
223
|
+
version: '0'
|
224
|
+
segments:
|
225
|
+
- 0
|
226
|
+
hash: 1151512606421308315
|
227
|
+
requirements: []
|
228
|
+
rubyforge_project: rds_backup_service
|
229
|
+
rubygems_version: 1.8.24
|
230
|
+
signing_key:
|
231
|
+
specification_version: 3
|
232
|
+
summary: Provides a REST API for backing up live RDS instances to S3 as a compressed
|
233
|
+
SQL file.
|
234
|
+
test_files: []
|