heroku-rds 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gemspec +18 -0
- data/LICENSE.txt +22 -0
- data/README.md +73 -0
- data/init.rb +1 -0
- data/lib/heroku/commands/rds.rb +283 -0
- data/lib/heroku/rds.rb +8 -0
- metadata +83 -0
data/.gemspec
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require 'rubygems' unless Object.const_defined?(:Gem)
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
s.name = "heroku-rds"
|
6
|
+
s.version = '0.4.0'
|
7
|
+
s.authors = ["Jonathan Dance"]
|
8
|
+
s.email = "rubygems@wuputah.com"
|
9
|
+
s.homepage = "http://github.com/wegowise/heroku-rds"
|
10
|
+
s.summary = "Heroku plugin to aid working with RDS databases"
|
11
|
+
s.description = "Heroku plugin to aid working with RDS databases"
|
12
|
+
s.required_rubygems_version = ">= 1.3.6"
|
13
|
+
s.add_dependency 'heroku', '~> 2.0'
|
14
|
+
s.add_dependency 'fog', '>= 0.7.0'
|
15
|
+
s.files = Dir.glob('lib/**/*.rb') + %w[README.md .gemspec init.rb]
|
16
|
+
s.extra_rdoc_files = ["README.md", "LICENSE.txt"]
|
17
|
+
s.license = 'MIT'
|
18
|
+
end
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
The MIT LICENSE
|
2
|
+
|
3
|
+
Copyright (c) 2011 Jonathan Dance
|
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,73 @@
|
|
1
|
+
## Installation
|
2
|
+
|
3
|
+
Forewarning: Heroku RDS is not designed to work on Windows.
|
4
|
+
|
5
|
+
### Gem-based install (recommended)
|
6
|
+
|
7
|
+
Gem-based plugins are new to the Heroku ecosystem, and require first
|
8
|
+
installing the [Herogems](https://github.com/hone/herogems) plugin:
|
9
|
+
|
10
|
+
heroku plugins:install http://github.com/hone/herogems.git
|
11
|
+
|
12
|
+
Now, simply install the gem and enable the plugin:
|
13
|
+
|
14
|
+
gem install heroku-rds
|
15
|
+
heroku herogems:enable heroku-rds
|
16
|
+
|
17
|
+
You can update to new releases of heroku-rds by running `gem update
|
18
|
+
heroku-rds`.
|
19
|
+
|
20
|
+
### Traditional install
|
21
|
+
|
22
|
+
heroku plugins:install http://github.com/wegowise/heroku-rds.git
|
23
|
+
gem install fog
|
24
|
+
|
25
|
+
To update, you must re-install the plugin using `heroku
|
26
|
+
plugins:install`.
|
27
|
+
|
28
|
+
### Optional Packages
|
29
|
+
|
30
|
+
Commands involving data transfer support a progress bar using `pv`.
|
31
|
+
Install `pv` to see the awesome. Most package managers have a pv
|
32
|
+
package:
|
33
|
+
|
34
|
+
brew install pv # OS X
|
35
|
+
apt-get install pv # linux/fink
|
36
|
+
port install pv # BSD/macports
|
37
|
+
|
38
|
+
## Usage
|
39
|
+
|
40
|
+
Access the command list at any time by typing `heroku help rds`:
|
41
|
+
|
42
|
+
Usage: heroku rds
|
43
|
+
|
44
|
+
Opens a MySQL console connected to the current database. Ingress access
|
45
|
+
is required to run this command (use rds:ingress to grant access).
|
46
|
+
|
47
|
+
Additional commands, type "heroku help COMMAND" for more details:
|
48
|
+
|
49
|
+
rds:access # displays current ingress access settings
|
50
|
+
rds:dump [FILE] # Download a database dump, bzipped and saved locally
|
51
|
+
rds:ingress [IP] [SECURITY GROUP] # Authorize ingress access to a particular IP
|
52
|
+
rds:pull [RAILS_ENV or DATABASE_URL] # downloads the remote database into a local database
|
53
|
+
rds:revoke [IP] [SECURITY GROUP] # Revokes previously-granted ingress access from a particular IP
|
54
|
+
|
55
|
+
## Planned features
|
56
|
+
|
57
|
+
### Short term
|
58
|
+
|
59
|
+
* rds:import - load a local database dump into the remote database
|
60
|
+
* rds:push - export a local database into the remote database
|
61
|
+
|
62
|
+
### Lower priority
|
63
|
+
|
64
|
+
* rds:snapshot - capture a snapshot
|
65
|
+
* rds:restore - restore from a snapshot
|
66
|
+
* rds:reboot - reboot instance
|
67
|
+
* rds:describe - show all RDS instances you have access to
|
68
|
+
* rds:scale - change instance size
|
69
|
+
|
70
|
+
These commands are not ingress related so the target of the command
|
71
|
+
cannot be inferred from DATABASE\_URL. This functionality is also
|
72
|
+
readily available from the RDS dashboard, so implementing them is not a
|
73
|
+
priority at this time.
|
data/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'heroku/rds'
|
@@ -0,0 +1,283 @@
|
|
1
|
+
require 'fog'
|
2
|
+
require 'uri'
|
3
|
+
|
4
|
+
module Heroku::Command
|
5
|
+
|
6
|
+
# manage Amazon RDS instances
|
7
|
+
#
|
8
|
+
class Rds < BaseWithApp
|
9
|
+
|
10
|
+
# rds
|
11
|
+
#
|
12
|
+
# Opens a MySQL console connected to the current database. Ingress access
|
13
|
+
# is required to run this command (use rds:ingress to grant access).
|
14
|
+
#
|
15
|
+
def index
|
16
|
+
check_dependencies('mysql')
|
17
|
+
exec *(['mysql', '--compress'] + mysql_args(database_uri))
|
18
|
+
end
|
19
|
+
|
20
|
+
# rds:dump [FILE]
|
21
|
+
#
|
22
|
+
# Download a database dump, bzipped and saved locally
|
23
|
+
#
|
24
|
+
# -f, --force # allow overwriting existing file
|
25
|
+
#
|
26
|
+
# if no FILE is specified, appname-date.sql.bz2 is used by default.
|
27
|
+
# if name of FILE does not end in .sql.bz2, it will be added automatically.
|
28
|
+
#
|
29
|
+
def dump
|
30
|
+
check_dependencies('mysqldump', 'bzip2', '/bin/sh')
|
31
|
+
options = {}
|
32
|
+
while arg = args.shift
|
33
|
+
case arg
|
34
|
+
when '-f', '--force'
|
35
|
+
options[:force] = true
|
36
|
+
when /^[^-]/
|
37
|
+
raise CommandFailed, "too many arguments passed" if options[:filename]
|
38
|
+
options[:filename] = arg
|
39
|
+
else
|
40
|
+
raise CommandFailed, "unsupported option: #{arg}"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
options[:filename] ||= "#{app}-#{Time.now.strftime('%Y-%m-%d')}.sql.bz2"
|
45
|
+
options[:filename] += '.sql.bz2' unless options[:filename] =~ /\.sql(\.bz2)?$/
|
46
|
+
options[:filename] += '.bz2' unless options[:filename] =~ /\.bz2$/
|
47
|
+
|
48
|
+
if File.exists?(options[:filename]) && !options[:force]
|
49
|
+
raise CommandFailed, "file already exists. use --force to override."
|
50
|
+
end
|
51
|
+
|
52
|
+
exec('/bin/sh', '-c',
|
53
|
+
"mysqldump --compress --single-transaction #{args_to_s(mysql_args(database_uri))}" +
|
54
|
+
pv_pipe +
|
55
|
+
%{| bzip2 > '#{options[:filename]}'})
|
56
|
+
end
|
57
|
+
|
58
|
+
# rds:ingress [IP] [SECURITY GROUP]
|
59
|
+
#
|
60
|
+
# Authorize ingress access to a particular IP
|
61
|
+
#
|
62
|
+
# * if IP is not specified, your current IP will be used
|
63
|
+
# * if SECURITY GROUP is not specified, 'default' will be used
|
64
|
+
# * IP can also be a CIDR range
|
65
|
+
#
|
66
|
+
def ingress
|
67
|
+
ip, security_group = parse_security_group_and_ip_from_args
|
68
|
+
rds.authorize_db_security_group_ingress(security_group, 'CIDRIP' => ip)
|
69
|
+
self.access
|
70
|
+
end
|
71
|
+
|
72
|
+
# rds:revoke [IP] [SECURITY GROUP]
|
73
|
+
#
|
74
|
+
# Revokes previously-granted ingress access from a particular IP
|
75
|
+
#
|
76
|
+
# * if IP is not specified, your current IP will be used
|
77
|
+
# * if SECURITY GROUP is not specified, 'default' will be used
|
78
|
+
# * IP can also be a CIDR range
|
79
|
+
#
|
80
|
+
def revoke
|
81
|
+
ip, security_group = parse_security_group_and_ip_from_args
|
82
|
+
rds.revoke_db_security_group_ingress(security_group, 'CIDRIP' => ip)
|
83
|
+
self.access
|
84
|
+
end
|
85
|
+
|
86
|
+
# rds:access
|
87
|
+
#
|
88
|
+
# displays current ingress access settings
|
89
|
+
#
|
90
|
+
def access
|
91
|
+
data = rds.security_groups.all.map do |group|
|
92
|
+
group.ec2_security_groups.map do |group_access|
|
93
|
+
[group.id, group_access['EC2SecurityGroupName'] + ' @ ' + group_access['EC2SecurityGroupOwnerId'], group_access['Status']]
|
94
|
+
end +
|
95
|
+
group.ip_ranges.map do |ip_range|
|
96
|
+
[group.id, ip_range['CIDRIP'], ip_range['Status']]
|
97
|
+
end
|
98
|
+
end.flatten(1)
|
99
|
+
data.unshift ['SECURITY GROUP', 'IP RANGE / SECURITY GROUP', 'STATUS']
|
100
|
+
lengths = (0..2).map { |i| data.map { |d| d[i].length }.max }
|
101
|
+
puts data.map { |d| '%-*s %-*s %-*s' % [lengths[0], d[0], lengths[1], d[1], lengths[2], d[2]] }.join("\n")
|
102
|
+
end
|
103
|
+
|
104
|
+
# rds:pull [RAILS_ENV or DATABASE_URL]
|
105
|
+
#
|
106
|
+
# downloads the remote database into a local database
|
107
|
+
#
|
108
|
+
# If a RAILS_ENV or DATABASE_URL is not specified, the current development environment
|
109
|
+
# is used (as read from config/database.yml). This command will confirm before executing
|
110
|
+
# the transfer.
|
111
|
+
#
|
112
|
+
def pull
|
113
|
+
check_dependencies('mysqldump', 'mysql')
|
114
|
+
|
115
|
+
target = args.shift || 'development'
|
116
|
+
if target =~ %r{://}
|
117
|
+
target = uri_to_hash(validate_db(URI.parse(target)))
|
118
|
+
else
|
119
|
+
raise CommandFailed, "config/database.yml not found" unless File.readable?("config/database.yml")
|
120
|
+
db_config = YAML.load(File.open("config/database.yml"))
|
121
|
+
raise CommandFailed, "environment #{target.inspect} not found in config/database.yml" unless
|
122
|
+
db_config.has_key?(target)
|
123
|
+
target = validate_db(db_config[target], target)
|
124
|
+
end
|
125
|
+
|
126
|
+
display "This will erase all data in the #{target['database'].inspect} database" +
|
127
|
+
(target['host'].empty? ? '' : " on #{target['host']}") + "!"
|
128
|
+
exit unless ask("Are you sure you wish to continue? [yN] ").downcase == 'y'
|
129
|
+
|
130
|
+
exec('/bin/sh', '-c',
|
131
|
+
'mysqldump --compress --single-transaction ' + args_to_s(mysql_args(database_uri)) +
|
132
|
+
pv_pipe +
|
133
|
+
%{| mysql --compress } + args_to_s(mysql_args(target)))
|
134
|
+
end
|
135
|
+
|
136
|
+
private
|
137
|
+
|
138
|
+
def current_ip
|
139
|
+
# simple rack app which returns your external IP
|
140
|
+
RestClient::Resource.new("http://ip4.heroku.com")['/'].get.strip
|
141
|
+
end
|
142
|
+
|
143
|
+
def ask(prompt = nil)
|
144
|
+
Readline.readline(prompt)
|
145
|
+
end
|
146
|
+
|
147
|
+
def parse_database_uri
|
148
|
+
URI.parse(heroku.config_vars(app)['DATABASE_URL'])
|
149
|
+
end
|
150
|
+
|
151
|
+
def mysql_args(creds)
|
152
|
+
creds = uri_to_hash(creds) if creds.is_a?(URI)
|
153
|
+
args = []
|
154
|
+
args.concat(['-u', creds['username']]) if creds['username'] && !creds['username'].empty?
|
155
|
+
args << "-p#{creds['password']}" if creds['password'] && !creds['password'].empty?
|
156
|
+
args.concat(['-h', creds['host']]) if creds['host'] && !creds['host'].empty?
|
157
|
+
args.concat(['-P', creds['port']]) if creds['port'] && !creds['port'].empty?
|
158
|
+
args.concat(['-S', creds['socket']]) if creds['socket'] && !creds['socket'].empty?
|
159
|
+
args << creds['database']
|
160
|
+
args
|
161
|
+
end
|
162
|
+
|
163
|
+
def args_to_s(args)
|
164
|
+
"'" + args.collect { |s| s.gsub("'", "\\'") }.join("' '") + "'"
|
165
|
+
end
|
166
|
+
|
167
|
+
def exec(*args)
|
168
|
+
ENV['DEBUG'] ? puts("exec(): #{args.inspect}") && exit : super
|
169
|
+
end
|
170
|
+
|
171
|
+
def system(*args)
|
172
|
+
exec = ENV['DEBUG'].nil?
|
173
|
+
unless exec
|
174
|
+
puts("system(): #{args.inspect}")
|
175
|
+
exec = ask("execute? [yN] ") == 'y'
|
176
|
+
end
|
177
|
+
if exec
|
178
|
+
super or raise CommandFailed, "command failed [code=#{$?.exitstatus}]: " + args.join(' ')
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
def validate_db(creds, name = nil)
|
183
|
+
if creds.is_a?(URI)
|
184
|
+
raise CommandFailed, "#{name || creds.to_s} is not a MySQL server" unless creds.scheme =~ /^mysql/
|
185
|
+
else
|
186
|
+
raise CommandFailed, "#{name || creds.inspect} is not a MySQL server" unless creds['adapter'] =~ /^mysql/
|
187
|
+
end
|
188
|
+
creds
|
189
|
+
end
|
190
|
+
|
191
|
+
def check_dependencies(*commands)
|
192
|
+
options = commands.last.is_a?(Hash) ? commands.pop : {}
|
193
|
+
results = commands.collect do |cmd|
|
194
|
+
path = `which #{cmd}`.strip
|
195
|
+
if !options[:optional]
|
196
|
+
raise CommandFailed, "#{cmd}: not found in path" if path.empty?
|
197
|
+
raise CommandFailed, "#{cmd}: not executable" unless File.executable?(path)
|
198
|
+
else
|
199
|
+
!path.empty? && File.executable?(path)
|
200
|
+
end
|
201
|
+
end
|
202
|
+
results.inject { |a, b| a && b }
|
203
|
+
end
|
204
|
+
|
205
|
+
def uri_to_hash(uri)
|
206
|
+
{ 'username' => uri.user,
|
207
|
+
'password' => uri.password,
|
208
|
+
'host' => uri.host,
|
209
|
+
'port' => uri.port,
|
210
|
+
'database' => uri.path.sub('/', '') }
|
211
|
+
end
|
212
|
+
|
213
|
+
def pv_installed?
|
214
|
+
check_dependencies('pv', :optional => true)
|
215
|
+
end
|
216
|
+
|
217
|
+
def pv_pipe
|
218
|
+
pv_installed? ? '| pv ' : ''
|
219
|
+
end
|
220
|
+
|
221
|
+
def database_uri
|
222
|
+
@database_uri ||= validate_db(parse_database_uri)
|
223
|
+
end
|
224
|
+
|
225
|
+
def aws_access_key_id
|
226
|
+
@aws_access_key_id ||= read_or_prompt_git_config('herokurds.accessKeyID', 'Please enter your AWS Access Key ID: ', 20)
|
227
|
+
end
|
228
|
+
|
229
|
+
def aws_secret_access_key
|
230
|
+
@aws_secret_access_key ||= read_or_prompt_git_config('herokurds.secretAccessKey', 'Please enter your AWS Secret Access Key: ', 40)
|
231
|
+
end
|
232
|
+
|
233
|
+
def read_or_prompt_git_config(config_var, prompt, length)
|
234
|
+
value = `git config #{config_var}`.strip
|
235
|
+
if value.empty?
|
236
|
+
value = ask(prompt)
|
237
|
+
unless value.length == length
|
238
|
+
puts "That is not valid; the value should be #{length} characters long."
|
239
|
+
return read_or_prompt_git_config(config_var, prompt, length)
|
240
|
+
end
|
241
|
+
system('git', 'config', '--add', config_var, value)
|
242
|
+
end
|
243
|
+
value
|
244
|
+
end
|
245
|
+
|
246
|
+
def rds
|
247
|
+
@rds ||= RdsProxy.new(:aws_access_key_id => aws_access_key_id, :aws_secret_access_key => aws_secret_access_key)
|
248
|
+
end
|
249
|
+
|
250
|
+
def parse_security_group_and_ip_from_args
|
251
|
+
ip = security_group = nil
|
252
|
+
while arg = args.shift
|
253
|
+
if arg =~ /^(?:\d{1,3}\.){3}\d{1,3}(?:\/\d{1,2})?$/
|
254
|
+
raise CommandFailed, "too many arguments passed" if ip
|
255
|
+
ip = arg
|
256
|
+
else
|
257
|
+
raise CommandFailed, "IP not in correct format or too many arguments passed" if security_group
|
258
|
+
security_group = arg
|
259
|
+
end
|
260
|
+
end
|
261
|
+
ip ||= current_ip + '/32'
|
262
|
+
ip += '/32' unless ip =~ /\/\d{1,2}$/
|
263
|
+
security_group ||= 'default'
|
264
|
+
[ip, security_group]
|
265
|
+
end
|
266
|
+
|
267
|
+
class RdsProxy
|
268
|
+
def initialize(*args)
|
269
|
+
@instance = Fog::AWS::RDS.new(*args)
|
270
|
+
end
|
271
|
+
|
272
|
+
private
|
273
|
+
def method_missing(sym, *args, &block)
|
274
|
+
begin
|
275
|
+
@instance.send(sym, *args, &block)
|
276
|
+
rescue Excon::Errors::HTTPStatusError => error
|
277
|
+
raise CommandFailed, Nokogiri::XML.parse(error.response.body).css('Message').first.content
|
278
|
+
end
|
279
|
+
end
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
end
|
data/lib/heroku/rds.rb
ADDED
metadata
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: heroku-rds
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease:
|
5
|
+
version: 0.4.0
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Jonathan Dance
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
|
13
|
+
date: 2011-06-01 00:00:00 -04:00
|
14
|
+
default_executable:
|
15
|
+
dependencies:
|
16
|
+
- !ruby/object:Gem::Dependency
|
17
|
+
name: heroku
|
18
|
+
prerelease: false
|
19
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
20
|
+
none: false
|
21
|
+
requirements:
|
22
|
+
- - ~>
|
23
|
+
- !ruby/object:Gem::Version
|
24
|
+
version: "2.0"
|
25
|
+
type: :runtime
|
26
|
+
version_requirements: *id001
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: fog
|
29
|
+
prerelease: false
|
30
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
31
|
+
none: false
|
32
|
+
requirements:
|
33
|
+
- - ">="
|
34
|
+
- !ruby/object:Gem::Version
|
35
|
+
version: 0.7.0
|
36
|
+
type: :runtime
|
37
|
+
version_requirements: *id002
|
38
|
+
description: Heroku plugin to aid working with RDS databases
|
39
|
+
email: rubygems@wuputah.com
|
40
|
+
executables: []
|
41
|
+
|
42
|
+
extensions: []
|
43
|
+
|
44
|
+
extra_rdoc_files:
|
45
|
+
- README.md
|
46
|
+
- LICENSE.txt
|
47
|
+
files:
|
48
|
+
- lib/heroku/commands/rds.rb
|
49
|
+
- lib/heroku/rds.rb
|
50
|
+
- README.md
|
51
|
+
- .gemspec
|
52
|
+
- init.rb
|
53
|
+
- LICENSE.txt
|
54
|
+
has_rdoc: true
|
55
|
+
homepage: http://github.com/wegowise/heroku-rds
|
56
|
+
licenses:
|
57
|
+
- MIT
|
58
|
+
post_install_message:
|
59
|
+
rdoc_options: []
|
60
|
+
|
61
|
+
require_paths:
|
62
|
+
- lib
|
63
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
64
|
+
none: false
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: "0"
|
69
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
70
|
+
none: false
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: 1.3.6
|
75
|
+
requirements: []
|
76
|
+
|
77
|
+
rubyforge_project:
|
78
|
+
rubygems_version: 1.6.2
|
79
|
+
signing_key:
|
80
|
+
specification_version: 3
|
81
|
+
summary: Heroku plugin to aid working with RDS databases
|
82
|
+
test_files: []
|
83
|
+
|