cap-taffy 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/History.txt ADDED
@@ -0,0 +1,9 @@
1
+ == 0.0.2 / 2009-09-14
2
+ * 1 minor enhancement
3
+ * Added authorizing SSH public keys on remote server(s).
4
+
5
+ == 0.0.1 / 2009-09-14
6
+
7
+ * 1 major enhancement
8
+ * Initial release.
9
+ * Added db push/pull.
data/README.md ADDED
@@ -0,0 +1,83 @@
1
+ Capistrano Taffy (Database, SSH recipes)
2
+ ================================================
3
+
4
+ **Capistrano recipes for deploying databases and other common tasks (managing database.yml, importing/exporting/transfering databases, SSH authorization etc.)**
5
+
6
+ Features
7
+ ------------------------------------------------
8
+
9
+ * Adds database transfer recipes (via [`Taps`]("http://github.com/ricardochimal/taps"))
10
+ * Authorize SSH access
11
+ * Manage `database.yml` (Soon.)
12
+
13
+ [`Taps`]("http://github.com/ricardochimal/taps") is great, but having to SSH into my deployment to run the `Taps` server, as well as
14
+ figure out the proper local/remote database urls, is a pain. I knew the [`Heroku`]("http://github.com/heroku/heroku") gem
15
+ had it figured out; I present the Capistrano friendly version.
16
+
17
+ If you are new to `Taps`, check out this [introduction to `Taps`]("http://adam.blog.heroku.com/past/2009/2/11/taps_for_easy_database_transfers/") by [Adam Wiggins]("http://github.com/adamwiggins").
18
+
19
+ Installation
20
+ ------------------------------------------------
21
+
22
+ gem install cap-taffy
23
+
24
+ Database Transfer
25
+ ------------------------------------------------
26
+
27
+ > _Dependency:_ The [`Taps`]("http://github.com/ricardochimal/taps") gem is required on any server(s) you'll be transferring databases to (`:app` role) including your development machine (where you'll be running `cap` tasks from). Run:
28
+
29
+ > gem install taps
30
+
31
+ > _Dependency:_ The [`Heroku`]("http://github.com/heroku/heroku") gem is required on your development machine.
32
+
33
+ > gem install heroku
34
+
35
+ ### Usage
36
+
37
+ To start, add the following to your `Capfile`
38
+
39
+ require 'cap-taffy/db'
40
+
41
+ `Taffy` will start a `Taps` server on port 5000 by default, so make sure port 5000 (or w/e port you end up using) is open on your `:app` server(s)
42
+
43
+ Then you can use:
44
+
45
+ cap db:push # Or to specify a different port: cap db:push -s taps_port=4321
46
+ cap db:pull # Or to specify a different port: cap db:pull -s taps_port=4321
47
+
48
+
49
+ #### SSH Local Forwarding
50
+
51
+ > Some of you may not be able/want to open up ports on your servers. Instead, you can run:
52
+
53
+ > ssh -N -L[port]:127.0.0.1:[port] [user]@[remote-server]
54
+ > And then run:
55
+
56
+ > cap db:push -s taps_port=[port] -s local=true
57
+ > Substituing the appropriate values for [port], [user], and [remote-server].
58
+ > #### Sample Usage
59
+ > > ssh -N -L4321:127.0.0.1:4321 henry@load-test
60
+ > > cap db:push -s taps_port=4321 -s local=true
61
+
62
+ SSH Access
63
+ ------------------------------------------------
64
+
65
+ #### Usage
66
+
67
+ Add the following to your `Capfile`
68
+
69
+ require 'cap-taffy/ssh'
70
+
71
+ Using a public key generated from `ssh-keygen` (e.g. `ssh-keygen -t rsa`), to authorize access:
72
+
73
+ cap ssh:authorize # authorizes local public key for SSH access to remote server(s)
74
+
75
+
76
+ Managing `database.yml`
77
+ ------------------------------------------------
78
+
79
+ > Much needed and coming soon.
80
+
81
+ Credits
82
+ ------------------------------------------------
83
+ Thanks to developers of the [`Taps`]("http://adam.blog.heroku.com/past/2009/2/11/taps_for_easy_database_transfers/") gem and the [`Heroku`]("http://github.com/heroku/heroku") gem. And of course, Capistrano, awesome!
data/Rakefile ADDED
@@ -0,0 +1,34 @@
1
+ # Look in the tasks/setup.rb file for the various options that can be
2
+ # configured in this Rakefile. The .rake files in the tasks directory
3
+ # are where the options are used.
4
+
5
+ begin
6
+ require 'bones'
7
+ Bones.setup
8
+ rescue LoadError
9
+ begin
10
+ load 'tasks/setup.rb'
11
+ rescue LoadError
12
+ raise RuntimeError, '### please install the "bones" gem ###'
13
+ end
14
+ end
15
+
16
+ ensure_in_path 'lib'
17
+ require 'cap-taffy'
18
+
19
+ task :default => 'spec:run'
20
+
21
+ PROJ.name = 'cap-taffy'
22
+ PROJ.authors = 'Henry Hsu'
23
+ PROJ.email = 'henry@qlane.com'
24
+ PROJ.url = 'http://by.qlane.com'
25
+ PROJ.version = CapTaffy::VERSION
26
+ PROJ.rubyforge.name = 'cap-taffy'
27
+ PROJ.readme_file = "README.md"
28
+ PROJ.gem.dependencies = ['heroku', 'taps', 'capistrano']
29
+ PROJ.gem.development_dependencies << ["mocha"]
30
+ PROJ.description = "Capistrano recipes for deploying databases and other common tasks."
31
+ PROJ.summary = "Capistrano recipes for deploying databases (managing database.yml, importing/exporting/transfering databases, etc.)"
32
+ PROJ.ignore_file = '.gitignore'
33
+ PROJ.spec.opts << '--color'
34
+ PROJ.exclude << "bin"
data/cap-taffy.gemspec ADDED
@@ -0,0 +1,45 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = %q{cap-taffy}
5
+ s.version = "0.0.2"
6
+
7
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
8
+ s.authors = ["Henry Hsu"]
9
+ s.date = %q{2009-09-17}
10
+ s.description = %q{Capistrano recipes for deploying databases and other common tasks.}
11
+ s.email = %q{henry@qlane.com}
12
+ s.extra_rdoc_files = ["History.txt"]
13
+ s.files = ["History.txt", "README.md", "Rakefile", "cap-taffy.gemspec", "lib/cap-taffy.rb", "lib/cap-taffy/db.rb", "lib/cap-taffy/parse.rb", "lib/cap-taffy/ssh.rb", "spec/cap-taffy/db_spec.rb", "spec/cap-taffy/parse_spec.rb", "spec/cap-taffy/ssh_spec.rb", "spec/cap-taffy_spec.rb", "spec/spec.opts", "spec/spec_helper.rb"]
14
+ s.homepage = %q{http://by.qlane.com}
15
+ s.rdoc_options = ["--main", "README.md"]
16
+ s.require_paths = ["lib"]
17
+ s.rubyforge_project = %q{cap-taffy}
18
+ s.rubygems_version = %q{1.3.5}
19
+ s.summary = %q{Capistrano recipes for deploying databases (managing database.yml, importing/exporting/transfering databases, etc.)}
20
+
21
+ if s.respond_to? :specification_version then
22
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
23
+ s.specification_version = 3
24
+
25
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
26
+ s.add_runtime_dependency(%q<heroku>, [">= 0"])
27
+ s.add_runtime_dependency(%q<taps>, [">= 0"])
28
+ s.add_runtime_dependency(%q<capistrano>, [">= 0"])
29
+ s.add_development_dependency(%q<bones>, [">= 2.5.1"])
30
+ s.add_development_dependency(%q<mocha>, [">= 0"])
31
+ else
32
+ s.add_dependency(%q<heroku>, [">= 0"])
33
+ s.add_dependency(%q<taps>, [">= 0"])
34
+ s.add_dependency(%q<capistrano>, [">= 0"])
35
+ s.add_dependency(%q<bones>, [">= 2.5.1"])
36
+ s.add_dependency(%q<mocha>, [">= 0"])
37
+ end
38
+ else
39
+ s.add_dependency(%q<heroku>, [">= 0"])
40
+ s.add_dependency(%q<taps>, [">= 0"])
41
+ s.add_dependency(%q<capistrano>, [">= 0"])
42
+ s.add_dependency(%q<bones>, [">= 2.5.1"])
43
+ s.add_dependency(%q<mocha>, [">= 0"])
44
+ end
45
+ end
@@ -0,0 +1,239 @@
1
+ require 'rubygems'
2
+ begin
3
+ gem 'taps', '>= 0.2.8', '< 0.3.0'
4
+ require 'taps/client_session'
5
+ rescue LoadError
6
+ error "Install the Taps gem to use db commands. On most systems this will be:\nsudo gem install taps"
7
+ end
8
+
9
+ require File.join(File.dirname(__FILE__), %w[.. cap-taffy]) unless defined?(CapTaffy)
10
+ require File.join(File.dirname(__FILE__), 'parse')
11
+ require 'digest/sha1'
12
+
13
+ module CapTaffy::Db
14
+ extend self
15
+
16
+ # Detects the local database url for +env+.
17
+ #
18
+ # Looks for <tt>config/database.yml</tt>.
19
+ def local_database_url(env)
20
+ return "" unless File.exists?(Dir.pwd + '/config/database.yml')
21
+ db_config = YAML.load(File.read(Dir.pwd + '/config/database.yml'))
22
+
23
+ CapTaffy::Parse.database_url(db_config, env)
24
+ end
25
+
26
+ # Detects the remote database url for +env+ and the current Capistrano +instance+.
27
+ #
28
+ # Looks for <tt>config/database.yml</tt> in the +current_path+.
29
+ def remote_database_url(instance, env)
30
+ db_yml = instance.capture "cat #{instance.current_path}/config/database.yml"
31
+ db_config = YAML::load(db_yml)
32
+
33
+ CapTaffy::Parse.database_url(db_config, env)
34
+ end
35
+
36
+ # The default server port the Taps server is started on.
37
+ def default_server_port
38
+ 5000
39
+ end
40
+
41
+ # Generates the remote url used by Taps push/pull.
42
+ #
43
+ # ==== Parameters
44
+ #
45
+ # * <tt>:login, :password, :host, :port</tt> - See #run.
46
+ #
47
+ # ==== Examples
48
+ #
49
+ # login = fetch(:user)
50
+ # password = tmp_pass(login) # returns asdkf239udjhdaks (for example)
51
+ # remote_url(:login => login, :password => password, :host => 'load-test') # returns http://henry:asdkf239udjhdaks@load-test:5000
52
+ def remote_url(options={})
53
+ host = options[:host]
54
+ port = options[:port] || default_server_port
55
+ host += ":#{port}"
56
+ url = CapTaffy::Parse.new.uri_hash_to_url('username' => options[:login], 'password' => options[:password], 'host' => host, 'scheme' => 'http', 'path' => '')
57
+
58
+ url.sub(/\/$/, '')
59
+ end
60
+
61
+ # Generates a temporary password to be used for the Taps server command.
62
+ def tmp_pass(user)
63
+ Digest::SHA1.hexdigest("--#{Time.now.to_s}--#{user}--")
64
+ end
65
+
66
+ # A quick start for a Taps client.
67
+ #
68
+ # <tt>local_database_url</tt> and <tt>remote_url</tt> refer to the options for the Taps gem (see #run).
69
+ def taps_client(local_database_url, remote_url, &blk) # :yields: client
70
+ Taps::Config.chunksize = 1000
71
+ Taps::Config.database_url = local_database_url
72
+ Taps::Config.remote_url = remote_url
73
+ Taps::Config.verify_database_url
74
+
75
+ Taps::ClientSession.quickstart do |client|
76
+ yield client
77
+ end
78
+ end
79
+
80
+ # Generates the server command used to start a Taps server
81
+ #
82
+ # ==== Parameters
83
+ # * <tt>:remote_database_url, :login, :password</tt> - See #run.
84
+ # * <tt>:port</tt> - The +port+ the Taps server is on. If given and different from #default_server_port, appends <tt>--port=[port]</tt> to command.
85
+ def server_command(options={})
86
+ remote_database_url, login, password, port = options[:remote_database_url], options[:login], options[:password], options[:port]
87
+ port_argument = ''
88
+ port_argument = " --port=#{port}" if port && port != default_server_port
89
+
90
+ "taps server #{remote_database_url} #{login} #{password}#{port_argument}"
91
+ end
92
+
93
+ # The meat of the operation. Runs operations after setting up the Taps server.
94
+ #
95
+ # 1. Runs the <tt>taps</tt> taps command to start the Taps server (assuming Sinatra is running on Thin)
96
+ # 2. Wait until the server is ready
97
+ # 3. Execute block on Taps client
98
+ # 4. Close the connection(s) and bid farewell.
99
+ #
100
+ # ==== Parameters
101
+ # * <tt>:remote_database_url</tt> - Refers to local database url in the options for the Taps server command (see Taps Options).
102
+ # * <tt>:login</tt> - The login for +host+. Usually what's in <tt>set :user, "the user"</tt> in <tt>deploy.rb</tt>
103
+ # * <tt>:password</tt> - The temporary password for the Taps server.
104
+ # * <tt>:port</tt> - The +port+ the Taps server is on. If not given, defaults to #default_server_port.
105
+ # * <tt>:local_database_url</tt> - Refers to the local database url in the options for Taps client commands (see Taps Options).
106
+ #
107
+ # ==== Taps Options
108
+ #
109
+ # <tt>taps</tt>
110
+ # server <local_database_url> <login> <password> [--port=N] Start a taps database import/export server
111
+ # pull <local_database_url> <remote_url> [--chunksize=N] Pull a database from a taps server
112
+ # push <local_database_url> <remote_url> [--chunksize=N] Push a database to a taps server
113
+ #
114
+ # ==== Examples
115
+ #
116
+ # task :push do
117
+ # login = fetch(:user)
118
+ # password = Time.now.to_s
119
+ # CapTaffy.Db.run(self, { :login => login, :password => password, :remote_database_url => "sqlite://test_production", :local_database_url => "sqlite://test_development" }) do |client|
120
+ # client.cmd_send
121
+ # end
122
+ # end
123
+ def run(instance, options = {} , &blk) # :yields: client
124
+ options[:port] ||= default_server_port
125
+ remote_database_url, login, password, port, local_database_url = options[:remote_database_url], options[:login], options[:password], options[:port], options[:local_database_url]
126
+ force_local = options.delete(:local)
127
+
128
+ data_so_far = ""
129
+ instance.run CapTaffy::Db.server_command(options) do |channel, stream, data|
130
+ data_so_far << data
131
+ if data_so_far.include? ">> Listening on 0.0.0.0:#{port}, CTRL+C to stop"
132
+ host = force_local ? '127.0.0.1' : channel[:host]
133
+ remote_url = CapTaffy::Db.remote_url(options.merge(:host => host))
134
+
135
+ CapTaffy::Db.taps_client(local_database_url, remote_url) do |client|
136
+ yield client
137
+ end
138
+
139
+ data_so_far = ""
140
+ channel.close
141
+ channel[:status] = 0
142
+ end
143
+ end
144
+ end
145
+
146
+ class InvalidURL < RuntimeError # :nodoc:
147
+ end
148
+ end
149
+
150
+ Capistrano::Configuration.instance.load do
151
+ namespace :db do
152
+ # Executes given block.
153
+ # If this is a dry run, any raised exceptions will be caught and +returning+ is returned.
154
+ # If this is not a dry run, any exceptions will be raised as expected.
155
+ def dry_run_safe(returning = nil, &block) # :yields:
156
+ begin
157
+ yield
158
+ rescue Exception => e
159
+ raise e unless dry_run
160
+ return returning
161
+ end
162
+ end
163
+
164
+ task :detect, :roles => :app do
165
+ @remote_database_url = dry_run_safe('') { CapTaffy::Db.remote_database_url(self, 'production') }
166
+ @local_database_url = dry_run_safe('') { CapTaffy::Db.local_database_url('development') }
167
+ end
168
+
169
+ desc <<-DESC
170
+ Push a local database into the app's remote database.
171
+
172
+ Performs push from local development database to remote production database.
173
+ Opens a Taps server on port 5000. (Ensure port is opened on the remote server).
174
+
175
+ # alternately, specify a different port
176
+ cap db:push -s taps_port=4321
177
+
178
+ For the security conscious:
179
+
180
+ # use ssh local forwarding (ensure [port] is available on both endpoints)
181
+ ssh -N -L[port]:127.0.0.1:[port] [user]@[remote-server]
182
+
183
+ # then push locally
184
+ cap db:push -s taps_port=[port] -s local=true
185
+ DESC
186
+ task :push, :roles => :app do
187
+ detect
188
+
189
+ login = fetch(:user)
190
+ password = CapTaffy::Db.tmp_pass(login)
191
+
192
+ logger = Capistrano::Logger.new
193
+ logger.important "Auto-detected remote database: #{@remote_database_url}" if @remote_database_url != ''
194
+ logger.important "Auto-detected local database: #{@local_database_url}" if @local_database_url != ''
195
+
196
+ options = {:remote_database_url => @remote_database_url, :login => login, :password => password, :local_database_url => @local_database_url, :port => variables[:taps_port]}
197
+ options.merge!(:local => true) if variables[:local]
198
+
199
+ CapTaffy::Db.run(self, options) do |client|
200
+ client.cmd_send
201
+ end
202
+ end
203
+
204
+ desc <<-DESC
205
+ Pull the app's database into a local database.
206
+
207
+ Performs pull from remote production database to local development database.
208
+ Opens a Taps server on port 5000. (Ensure port is opened on the remote server).
209
+
210
+ # alternately, specify a different port
211
+ cap db:pull -s taps_port=4321
212
+
213
+ For the security conscious:
214
+
215
+ # use ssh local forwarding (ensure [port] is available on both endpoints)
216
+ ssh -N -L[port]:127.0.0.1:[port] [user]@[remote-server]
217
+
218
+ # then pull locally
219
+ cap db:pull -s taps_port=[port] -s local=true
220
+ DESC
221
+ task :pull, :roles => :app do
222
+ detect
223
+
224
+ login = fetch(:user)
225
+ password = CapTaffy::Db.tmp_pass(login)
226
+
227
+ logger = Capistrano::Logger.new
228
+ logger.important "Auto-detected remote database: #{@remote_database_url}" if @remote_database_url != ''
229
+ logger.important "Auto-detected local database: #{@local_database_url}" if @local_database_url != ''
230
+
231
+ options = {:remote_database_url => @remote_database_url, :login => login, :password => password, :local_database_url => @local_database_url, :port => variables[:taps_port]}
232
+ options.merge!(:local => true) if variables[:local]
233
+
234
+ CapTaffy::Db.run(self, options) do |client|
235
+ client.cmd_receive
236
+ end
237
+ end
238
+ end
239
+ end
@@ -0,0 +1,53 @@
1
+ require 'rubygems'
2
+ begin
3
+ require 'heroku'
4
+ require 'heroku/commands/base'
5
+ require 'heroku/commands/db'
6
+ rescue LoadError
7
+ error "Install the Heroku gem. On most systems this will be:\nsudo gem install taps"
8
+ end
9
+
10
+ module CapTaffy
11
+ class Parse < Heroku::Command::Db
12
+ class << self
13
+ attr_accessor :instance
14
+
15
+ # Modified from :parse_database_yml in heroku/command/db.rb
16
+ #
17
+ # Accepts a complete +db_config+ hash and +env+ and parses for a database_url accordingly.
18
+ def database_url(db_config, env)
19
+ raise Invalid, "please pass me a valid Hash loaded from a database YAML file" unless db_config
20
+ conf = db_config[env]
21
+ raise Invalid, "missing '#{env}' database in #{db_config.inspect}" unless conf
22
+
23
+ self.instance ||= CapTaffy::Parse.new
24
+
25
+ case conf['adapter']
26
+ when 'sqlite3'
27
+ return "sqlite://#{conf['database']}"
28
+ when 'postgresql'
29
+ uri_hash = self.instance.conf_to_uri_hash(conf)
30
+ uri_hash['scheme'] = 'postgres'
31
+ return self.instance.uri_hash_to_url(uri_hash)
32
+ else
33
+ return self.instance.uri_hash_to_url(self.instance.conf_to_uri_hash(conf))
34
+ end
35
+ end
36
+ end
37
+
38
+ # Override to do nothing on #new
39
+ def initialize # :nodoc:
40
+
41
+ end
42
+
43
+ # Override to pass-through on #escape
44
+ def escape(string)
45
+ string
46
+ end
47
+
48
+ public :uri_hash_to_url, :conf_to_uri_hash
49
+
50
+ class Invalid < RuntimeError # :nodoc:
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,18 @@
1
+ require File.join(File.dirname(__FILE__), %w[.. cap-taffy]) unless defined?(CapTaffy)
2
+
3
+ module CapTaffy::SSH
4
+
5
+ end
6
+
7
+ Capistrano::Configuration.instance.load do
8
+ namespace :ssh do
9
+ desc <<-DESC
10
+ Authorize SSH access for local computer on remote computers(s).
11
+ DESC
12
+ task :authorize do
13
+ public_key = File.read(File.expand_path(File.join(%w[~/ .ssh id_rsa.pub]))).chop
14
+
15
+ run %Q[if [ ! -f ~/.ssh/authorized_keys ]; then mkdir -p ~/.ssh && touch ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys; fi && if [ -z "$(grep "^#{public_key}$" ~/.ssh/authorized_keys)" ]; then echo "#{public_key}" >> ~/.ssh/authorized_keys && echo "Public key on '$CAPISTRANO:HOST$' authorized at '#{Time.now.to_s}'"; else echo "Public key on '$CAPISTRANO:HOST$' is already authorized."; fi]
16
+ end
17
+ end
18
+ end
data/lib/cap-taffy.rb ADDED
@@ -0,0 +1,45 @@
1
+
2
+ module CapTaffy
3
+
4
+ # :stopdoc:
5
+ VERSION = '0.0.2'
6
+ LIBPATH = ::File.expand_path(::File.dirname(__FILE__)) + ::File::SEPARATOR
7
+ PATH = ::File.dirname(LIBPATH) + ::File::SEPARATOR
8
+ # :startdoc:
9
+
10
+ # Returns the version string for the library.
11
+ #
12
+ def self.version
13
+ VERSION
14
+ end
15
+
16
+ # Returns the library path for the module. If any arguments are given,
17
+ # they will be joined to the end of the libray path using
18
+ # <tt>File.join</tt>.
19
+ #
20
+ def self.libpath( *args )
21
+ args.empty? ? LIBPATH : ::File.join(LIBPATH, args.flatten)
22
+ end
23
+
24
+ # Returns the lpath for the module. If any arguments are given,
25
+ # they will be joined to the end of the path using
26
+ # <tt>File.join</tt>.
27
+ #
28
+ def self.path( *args )
29
+ args.empty? ? PATH : ::File.join(PATH, args.flatten)
30
+ end
31
+
32
+ # Utility method used to require all files ending in .rb that lie in the
33
+ # directory below this file that has the same name as the filename passed
34
+ # in. Optionally, a specific _directory_ name can be passed in such that
35
+ # the _filename_ does not have to be equivalent to the directory.
36
+ #
37
+ def self.require_all_libs_relative_to( fname, dir = nil )
38
+ dir ||= ::File.basename(fname, '.*')
39
+ search_me = ::File.expand_path(
40
+ ::File.join(::File.dirname(fname), dir, '**', '*.rb'))
41
+
42
+ Dir.glob(search_me).sort.each {|rb| require rb}
43
+ end
44
+
45
+ end