cap-taffy 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
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