heroku-rds 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,8 @@
1
+ # ghetto heroku gem dependency management
2
+ if Heroku::VERSION < '2.0.0'
3
+ puts "Please upgrade your Heroku gem"
4
+ elsif Heroku::VERSION >= '3.0.0'
5
+ puts "Please update your heroku-rds plugin (or find a new maintainer)"
6
+ else
7
+ require 'heroku/commands/rds'
8
+ end
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
+