capistrano 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. data/bin/cap +11 -0
  2. data/examples/sample.rb +113 -0
  3. data/lib/capistrano.rb +1 -0
  4. data/lib/capistrano/actor.rb +438 -0
  5. data/lib/capistrano/cli.rb +295 -0
  6. data/lib/capistrano/command.rb +90 -0
  7. data/lib/capistrano/configuration.rb +243 -0
  8. data/lib/capistrano/extensions.rb +38 -0
  9. data/lib/capistrano/gateway.rb +118 -0
  10. data/lib/capistrano/generators/rails/deployment/deployment_generator.rb +25 -0
  11. data/lib/capistrano/generators/rails/deployment/templates/capistrano.rake +46 -0
  12. data/lib/capistrano/generators/rails/deployment/templates/deploy.rb +122 -0
  13. data/lib/capistrano/generators/rails/loader.rb +20 -0
  14. data/lib/capistrano/logger.rb +59 -0
  15. data/lib/capistrano/recipes/standard.rb +242 -0
  16. data/lib/capistrano/recipes/templates/maintenance.rhtml +53 -0
  17. data/lib/capistrano/scm/base.rb +62 -0
  18. data/lib/capistrano/scm/baz.rb +118 -0
  19. data/lib/capistrano/scm/bzr.rb +70 -0
  20. data/lib/capistrano/scm/cvs.rb +124 -0
  21. data/lib/capistrano/scm/darcs.rb +27 -0
  22. data/lib/capistrano/scm/perforce.rb +139 -0
  23. data/lib/capistrano/scm/subversion.rb +122 -0
  24. data/lib/capistrano/ssh.rb +39 -0
  25. data/lib/capistrano/transfer.rb +90 -0
  26. data/lib/capistrano/utils.rb +26 -0
  27. data/lib/capistrano/version.rb +30 -0
  28. data/test/actor_test.rb +294 -0
  29. data/test/command_test.rb +43 -0
  30. data/test/configuration_test.rb +233 -0
  31. data/test/fixtures/config.rb +5 -0
  32. data/test/fixtures/custom.rb +3 -0
  33. data/test/scm/cvs_test.rb +186 -0
  34. data/test/scm/subversion_test.rb +137 -0
  35. data/test/ssh_test.rb +104 -0
  36. data/test/utils.rb +50 -0
  37. metadata +107 -0
@@ -0,0 +1,295 @@
1
+ require 'optparse'
2
+ require 'capistrano'
3
+
4
+ module Capistrano
5
+ # The CLI class encapsulates the behavior of capistrano when it is invoked
6
+ # as a command-line utility. This allows other programs to embed ST and
7
+ # preserve it's command-line semantics.
8
+ class CLI
9
+ # Invoke capistrano using the ARGV array as the option parameters. This
10
+ # is what the command-line capistrano utility does.
11
+ def self.execute!
12
+ new.execute!
13
+ end
14
+
15
+ # The following determines whether or not echo-suppression is available.
16
+ # This requires the termios library to be installed (which, unfortunately,
17
+ # is not available for Windows).
18
+ begin
19
+ if !defined?(USE_TERMIOS) || USE_TERMIOS
20
+ require 'termios'
21
+ else
22
+ raise LoadError
23
+ end
24
+
25
+ # Enable or disable stdin echoing to the terminal.
26
+ def self.echo(enable)
27
+ term = Termios::getattr(STDIN)
28
+
29
+ if enable
30
+ term.c_lflag |= (Termios::ECHO | Termios::ICANON)
31
+ else
32
+ term.c_lflag &= ~Termios::ECHO
33
+ end
34
+
35
+ Termios::setattr(STDIN, Termios::TCSANOW, term)
36
+ end
37
+ rescue LoadError
38
+ def self.echo(enable)
39
+ end
40
+ end
41
+
42
+ # execute the associated block with echo-suppression enabled. Note that
43
+ # if termios is not available, echo suppression will not be available
44
+ # either.
45
+ def self.with_echo
46
+ echo(false)
47
+ yield
48
+ ensure
49
+ echo(true)
50
+ end
51
+
52
+ # Prompt for a password using echo suppression.
53
+ def self.password_prompt(prompt="Password: ")
54
+ sync = STDOUT.sync
55
+ begin
56
+ with_echo do
57
+ STDOUT.sync = true
58
+ print(prompt)
59
+ STDIN.gets.chomp
60
+ end
61
+ ensure
62
+ STDOUT.sync = sync
63
+ puts
64
+ end
65
+ end
66
+
67
+ # The array of (unparsed) command-line options
68
+ attr_reader :args
69
+
70
+ # The hash of (parsed) command-line options
71
+ attr_reader :options
72
+
73
+ # Create a new CLI instance using the given array of command-line parameters
74
+ # to initialize it. By default, +ARGV+ is used, but you can specify a
75
+ # different set of parameters (such as when embedded ST in a program):
76
+ #
77
+ # require 'capistrano/cli'
78
+ # Capistrano::CLI.new(%w(-vvvv -r config/deploy -a update_code)).execute!
79
+ #
80
+ # Note that you can also embed ST directly by creating a new Configuration
81
+ # instance and setting it up, but you'll often wind up duplicating logic
82
+ # defined in the CLI class. The above snippet, redone using the Configuration
83
+ # class directly, would look like:
84
+ #
85
+ # require 'capistrano'
86
+ # require 'capistrano/cli'
87
+ # config = Capistrano::Configuration.new
88
+ # config.logger_level = Capistrano::Logger::TRACE
89
+ # config.set(:password) { Capistrano::CLI.password_prompt }
90
+ # config.load "standard", "config/deploy"
91
+ # config.actor.update_code
92
+ #
93
+ # There may be times that you want/need the additional control offered by
94
+ # manipulating the Configuration directly, but generally interfacing with
95
+ # the CLI class is recommended.
96
+ def initialize(args = ARGV)
97
+ @args = args
98
+ @options = { :recipes => [], :actions => [], :vars => {},
99
+ :pre_vars => {} }
100
+
101
+ OptionParser.new do |opts|
102
+ opts.banner = "Usage: #{$0} [options] [args]"
103
+
104
+ opts.separator ""
105
+ opts.separator "Recipe Options -----------------------"
106
+ opts.separator ""
107
+
108
+ opts.on("-a", "--action ACTION",
109
+ "An action to execute. Multiple actions may",
110
+ "be specified, and are loaded in the given order."
111
+ ) { |value| @options[:actions] << value }
112
+
113
+ opts.on("-p", "--password [PASSWORD]",
114
+ "The password to use when connecting. If the switch",
115
+ "is given without a password, the password will be",
116
+ "prompted for immediately. (Default: prompt for password",
117
+ "the first time it is needed.)"
118
+ ) { |value| @options[:password] = value }
119
+
120
+ opts.on("-r", "--recipe RECIPE",
121
+ "A recipe file to load. Multiple recipes may",
122
+ "be specified, and are loaded in the given order."
123
+ ) { |value| @options[:recipes] << value }
124
+
125
+ opts.on("-s", "--set NAME=VALUE",
126
+ "Specify a variable and it's value to set. This",
127
+ "will be set after loading all recipe files."
128
+ ) do |pair|
129
+ name, value = pair.split(/=/, 2)
130
+ @options[:vars][name.to_sym] = value
131
+ end
132
+
133
+ opts.on("-S", "--set-before NAME=VALUE",
134
+ "Specify a variable and it's value to set. This",
135
+ "will be set BEFORE loading all recipe files."
136
+ ) do |pair|
137
+ name, value = pair.split(/=/, 2)
138
+ @options[:pre_vars][name.to_sym] = value
139
+ end
140
+
141
+ opts.separator ""
142
+ opts.separator "Framework Integration Options --------"
143
+ opts.separator ""
144
+
145
+ opts.on("-A", "--apply-to DIRECTORY",
146
+ "Create a minimal set of scripts and recipes to use",
147
+ "capistrano with the application at the given",
148
+ "directory. (Currently only works with Rails apps.)"
149
+ ) { |value| @options[:apply_to] = value }
150
+
151
+ opts.separator ""
152
+ opts.separator "Miscellaneous Options ----------------"
153
+ opts.separator ""
154
+
155
+ opts.on("-h", "--help", "Display this help message") do
156
+ puts opts
157
+ exit
158
+ end
159
+
160
+ opts.on("-P", "--[no-]pretend",
161
+ "Run the task(s), but don't actually connect to or",
162
+ "execute anything on the servers. (For various reasons",
163
+ "this will not necessarily be an accurate depiction",
164
+ "of the work that will actually be performed.",
165
+ "Default: don't pretend.)"
166
+ ) { |value| @options[:pretend] = value }
167
+
168
+ opts.on("-q", "--quiet",
169
+ "Make the output as quiet as possible (the default)"
170
+ ) { @options[:verbose] = 0 }
171
+
172
+ opts.on("-v", "--verbose",
173
+ "Specify the verbosity of the output.",
174
+ "May be given multiple times. (Default: silent)"
175
+ ) { @options[:verbose] ||= 0; @options[:verbose] += 1 }
176
+
177
+ opts.on("-V", "--version",
178
+ "Display the version info for this utility"
179
+ ) do
180
+ require 'capistrano/version'
181
+ puts "Capistrano v#{Capistrano::Version::STRING}"
182
+ exit
183
+ end
184
+
185
+ opts.separator ""
186
+ opts.separator <<-DETAIL.split(/\n/)
187
+ You can use the --apply-to switch to generate a minimal set of capistrano
188
+ scripts and recipes for an application. Just specify the path to the application
189
+ as the argument to --apply-to, like this:
190
+
191
+ capistrano --apply-to ~/projects/myapp
192
+
193
+ You'll wind up with a sample deployment recipe in config/deploy.rb, some new
194
+ rake tasks in config/tasks, and a capistrano script in your script directory.
195
+
196
+ (Currently, --apply-to only works with Rails applications.)
197
+ DETAIL
198
+
199
+ if args.empty?
200
+ puts opts
201
+ exit
202
+ else
203
+ opts.parse!(args)
204
+ end
205
+ end
206
+
207
+ check_options!
208
+
209
+ password_proc = Proc.new { self.class.password_prompt }
210
+
211
+ if !@options.has_key?(:password)
212
+ @options[:password] = password_proc
213
+ elsif !@options[:password]
214
+ @options[:password] = password_proc.call
215
+ end
216
+ end
217
+
218
+ # Beginning running Capistrano based on the configured options.
219
+ def execute!
220
+ if !@options[:recipes].empty?
221
+ execute_recipes!
222
+ elsif @options[:apply_to]
223
+ execute_apply_to!
224
+ end
225
+ end
226
+
227
+ private
228
+
229
+ # Load the recipes specified by the options, and execute the actions
230
+ # specified.
231
+ def execute_recipes!
232
+ config = Capistrano::Configuration.new
233
+ config.logger.level = options[:verbose]
234
+ config.set :password, options[:password]
235
+ config.set :pretend, options[:pretend]
236
+
237
+ options[:pre_vars].each { |name, value| config.set(name, value) }
238
+
239
+ # load the standard recipe definition
240
+ config.load "standard"
241
+
242
+ options[:recipes].each { |recipe| config.load(recipe) }
243
+ options[:vars].each { |name, value| config.set(name, value) }
244
+
245
+ actor = config.actor
246
+ options[:actions].each { |action| actor.send action }
247
+ end
248
+
249
+ # Load the Rails generator and apply it to the specified directory.
250
+ def execute_apply_to!
251
+ require 'capistrano/generators/rails/loader'
252
+ Generators::RailsLoader.load! @options
253
+ end
254
+
255
+ APPLY_TO_OPTIONS = [:apply_to]
256
+ RECIPE_OPTIONS = [:password]
257
+ DEFAULT_RECIPES = %w(Capfile capfile config/deploy.rb)
258
+
259
+ # A sanity check to ensure that a valid operation is specified.
260
+ def check_options!
261
+ # if no verbosity has been specified, be verbose
262
+ @options[:verbose] = 3 if !@options.has_key?(:verbose)
263
+
264
+ apply_to_given = !(@options.keys & APPLY_TO_OPTIONS).empty?
265
+ recipe_given = !(@options.keys & RECIPE_OPTIONS).empty? ||
266
+ !@options[:recipes].empty? ||
267
+ !@options[:actions].empty?
268
+
269
+ if apply_to_given && recipe_given
270
+ abort "You cannot specify both recipe options and framework integration options."
271
+ elsif !apply_to_given
272
+ look_for_default_recipe_file! if @options[:recipes].empty?
273
+ look_for_raw_actions!
274
+ abort "You must specify at least one recipe" if @options[:recipes].empty?
275
+ abort "You must specify at least one action" if @options[:actions].empty?
276
+ else
277
+ @options[:application] = args.shift
278
+ @options[:recipe_file] = args.shift
279
+ end
280
+ end
281
+
282
+ def look_for_default_recipe_file!
283
+ DEFAULT_RECIPES.each do |file|
284
+ if File.exist?(file)
285
+ @options[:recipes] << file
286
+ break
287
+ end
288
+ end
289
+ end
290
+
291
+ def look_for_raw_actions!
292
+ @options[:actions].concat(@args)
293
+ end
294
+ end
295
+ end
@@ -0,0 +1,90 @@
1
+ module Capistrano
2
+
3
+ # This class encapsulates a single command to be executed on a set of remote
4
+ # machines, in parallel.
5
+ class Command
6
+ attr_reader :servers, :command, :options, :actor
7
+
8
+ def initialize(servers, command, callback, options, actor) #:nodoc:
9
+ @servers = servers
10
+ @command = command.strip.gsub(/\r?\n/, "\\\n")
11
+ @callback = callback
12
+ @options = options
13
+ @actor = actor
14
+ @channels = open_channels
15
+ end
16
+
17
+ def logger #:nodoc:
18
+ actor.logger
19
+ end
20
+
21
+ # Processes the command in parallel on all specified hosts. If the command
22
+ # fails (non-zero return code) on any of the hosts, this will raise a
23
+ # RuntimeError.
24
+ def process!
25
+ since = Time.now
26
+ loop do
27
+ active = 0
28
+ @channels.each do |ch|
29
+ next if ch[:closed]
30
+ active += 1
31
+ ch.connection.process(true)
32
+ end
33
+
34
+ break if active == 0
35
+ if Time.now - since >= 1
36
+ since = Time.now
37
+ @channels.each { |ch| ch.connection.ping! }
38
+ end
39
+ sleep 0.01 # a brief respite, to keep the CPU from going crazy
40
+ end
41
+
42
+ logger.trace "command finished"
43
+
44
+ if failed = @channels.detect { |ch| ch[:status] != 0 }
45
+ raise "command #{@command.inspect} failed on #{failed[:host]}"
46
+ end
47
+
48
+ self
49
+ end
50
+
51
+ private
52
+
53
+ def open_channels
54
+ @servers.map do |server|
55
+ @actor.sessions[server].open_channel do |channel|
56
+ channel[:host] = server
57
+ channel[:actor] = @actor # so callbacks can access the actor instance
58
+ channel.request_pty :want_reply => true
59
+
60
+ channel.on_success do |ch|
61
+ logger.trace "executing command", ch[:host]
62
+ ch.exec command
63
+ ch.send_data options[:data] if options[:data]
64
+ end
65
+
66
+ channel.on_failure do |ch|
67
+ logger.important "could not open channel", ch[:host]
68
+ ch.close
69
+ end
70
+
71
+ channel.on_data do |ch, data|
72
+ @callback[ch, :out, data] if @callback
73
+ end
74
+
75
+ channel.on_extended_data do |ch, type, data|
76
+ @callback[ch, :err, data] if @callback
77
+ end
78
+
79
+ channel.on_request do |ch, request, reply, data|
80
+ ch[:status] = data.read_long if request == "exit-status"
81
+ end
82
+
83
+ channel.on_close do |ch|
84
+ ch[:closed] = true
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,243 @@
1
+ require 'capistrano/actor'
2
+ require 'capistrano/logger'
3
+ require 'capistrano/scm/subversion'
4
+ require 'capistrano/extensions'
5
+
6
+ module Capistrano
7
+ # Represents a specific Capistrano configuration. A Configuration instance
8
+ # may be used to load multiple recipe files, define and describe tasks,
9
+ # define roles, create an actor, and set configuration variables.
10
+ class Configuration
11
+ Role = Struct.new(:host, :options)
12
+
13
+ DEFAULT_VERSION_DIR_NAME = "releases" #:nodoc:
14
+ DEFAULT_CURRENT_DIR_NAME = "current" #:nodoc:
15
+ DEFAULT_SHARED_DIR_NAME = "shared" #:nodoc:
16
+
17
+ # The actor created for this configuration instance.
18
+ attr_reader :actor
19
+
20
+ # The list of Role instances defined for this configuration.
21
+ attr_reader :roles
22
+
23
+ # The logger instance defined for this configuration.
24
+ attr_reader :logger
25
+
26
+ # The load paths used for locating recipe files.
27
+ attr_reader :load_paths
28
+
29
+ # The time (in UTC) at which this configuration was created, used for
30
+ # determining the release path.
31
+ attr_reader :now
32
+
33
+ # The has of variables currently known by the configuration
34
+ attr_reader :variables
35
+
36
+ def initialize(actor_class=Actor) #:nodoc:
37
+ @roles = Hash.new { |h,k| h[k] = [] }
38
+ @actor = actor_class.new(self)
39
+ @logger = Logger.new
40
+ @load_paths = [".", File.join(File.dirname(__FILE__), "recipes")]
41
+ @variables = {}
42
+ @now = Time.now.utc
43
+
44
+ # for preserving the original value of Proc-valued variables
45
+ set :original_value, Hash.new
46
+
47
+ set :application, nil
48
+ set :repository, nil
49
+ set :gateway, nil
50
+ set :user, nil
51
+ set :password, nil
52
+
53
+ set :ssh_options, Hash.new
54
+
55
+ set(:deploy_to) { "/u/apps/#{application}" }
56
+
57
+ set :version_dir, DEFAULT_VERSION_DIR_NAME
58
+ set :current_dir, DEFAULT_CURRENT_DIR_NAME
59
+ set :shared_dir, DEFAULT_SHARED_DIR_NAME
60
+ set :scm, :subversion
61
+
62
+ set(:revision) { source.latest_revision }
63
+ end
64
+
65
+ # Set a variable to the given value.
66
+ def set(variable, value=nil, &block)
67
+ # if the variable is uppercase, then we add it as a constant to the
68
+ # actor. This is to allow uppercase "variables" to be set and referenced
69
+ # in recipes.
70
+ if variable.to_s[0].between?(?A, ?Z)
71
+ klass = @actor.metaclass
72
+ klass.send(:remove_const, variable) if klass.const_defined?(variable)
73
+ klass.const_set(variable, value)
74
+ end
75
+
76
+ value = block if value.nil? && block_given?
77
+ @variables[variable] = value
78
+ end
79
+
80
+ alias :[]= :set
81
+
82
+ # Access a named variable. If the value of the variable responds_to? :call,
83
+ # #call will be invoked (without parameters) and the return value cached
84
+ # and returned.
85
+ def [](variable)
86
+ if @variables[variable].respond_to?(:call)
87
+ self[:original_value][variable] = @variables[variable]
88
+ set variable, @variables[variable].call
89
+ end
90
+ @variables[variable]
91
+ end
92
+
93
+ # Based on the current value of the <tt>:scm</tt> variable, instantiate and
94
+ # return an SCM module representing the desired source control behavior.
95
+ def source
96
+ @source ||= case scm
97
+ when Class then
98
+ scm.new(self)
99
+ when String, Symbol then
100
+ require "capistrano/scm/#{scm.to_s.downcase}"
101
+ Capistrano::SCM.const_get(scm.to_s.downcase.capitalize).new(self)
102
+ else
103
+ raise "invalid scm specification: #{scm.inspect}"
104
+ end
105
+ end
106
+
107
+ # Load a configuration file or string into this configuration.
108
+ #
109
+ # Usage:
110
+ #
111
+ # load("recipe"):
112
+ # Look for and load the contents of 'recipe.rb' into this
113
+ # configuration.
114
+ #
115
+ # load(:file => "recipe"):
116
+ # same as above
117
+ #
118
+ # load(:string => "set :scm, :subversion"):
119
+ # Load the given string as a configuration specification.
120
+ #
121
+ # load { ... }
122
+ # Load the block in the context of the configuration.
123
+ def load(*args, &block)
124
+ options = args.last.is_a?(Hash) ? args.pop : {}
125
+ args.each { |arg| load options.merge(:file => arg) }
126
+ return unless args.empty?
127
+
128
+ if block
129
+ raise "loading a block requires 0 parameters" unless args.empty?
130
+ load(options.merge(:proc => block))
131
+
132
+ elsif options[:file]
133
+ file = options[:file]
134
+ unless file[0] == ?/
135
+ load_paths.each do |path|
136
+ if File.file?(File.join(path, file))
137
+ file = File.join(path, file)
138
+ break
139
+ elsif File.file?(File.join(path, file) + ".rb")
140
+ file = File.join(path, file + ".rb")
141
+ break
142
+ end
143
+ end
144
+ end
145
+
146
+ load :string => File.read(file), :name => options[:name] || file
147
+
148
+ elsif options[:string]
149
+ logger.trace "loading configuration #{options[:name] || "<eval>"}"
150
+ instance_eval(options[:string], options[:name] || "<eval>")
151
+
152
+ elsif options[:proc]
153
+ logger.trace "loading configuration #{options[:proc].inspect}"
154
+ instance_eval(&options[:proc])
155
+
156
+ else
157
+ raise ArgumentError, "don't know how to load #{options.inspect}"
158
+ end
159
+ end
160
+
161
+ # Define a new role and its associated servers. You must specify at least
162
+ # one host for each role. Also, you can specify additional information
163
+ # (in the form of a Hash) which can be used to more uniquely specify the
164
+ # subset of servers specified by this specific role definition.
165
+ #
166
+ # Usage:
167
+ #
168
+ # role :db, "db1.example.com", "db2.example.com"
169
+ # role :db, "master.example.com", :primary => true
170
+ # role :app, "app1.example.com", "app2.example.com"
171
+ def role(which, *args)
172
+ options = args.last.is_a?(Hash) ? args.pop : {}
173
+ raise ArgumentError, "must give at least one host" if args.empty?
174
+ args.each { |host| roles[which] << Role.new(host, options) }
175
+ end
176
+
177
+ # Describe the next task to be defined. The given text will be attached to
178
+ # the next task that is defined and used as its description.
179
+ def desc(text)
180
+ @next_description = text
181
+ end
182
+
183
+ # Define a new task. If a description is active (see #desc), it is added to
184
+ # the options under the <tt>:desc</tt> key. This method ultimately
185
+ # delegates to Actor#define_task.
186
+ def task(name, options={}, &block)
187
+ raise ArgumentError, "expected a block" unless block
188
+
189
+ if @next_description
190
+ options = options.merge(:desc => @next_description)
191
+ @next_description = nil
192
+ end
193
+
194
+ actor.define_task(name, options, &block)
195
+ end
196
+
197
+ # Require another file. This is identical to the standard require method,
198
+ # with the exception that it sets the reciever as the "current" configuration
199
+ # so that third-party task bundles can include themselves relative to
200
+ # that configuration.
201
+ def require(*args) #:nodoc:
202
+ original, Capistrano.configuration = Capistrano.configuration, self
203
+ super
204
+ ensure
205
+ # restore the original, so that require's can be nested
206
+ Capistrano.configuration = original
207
+ end
208
+
209
+ # Return the path into which releases should be deployed.
210
+ def releases_path
211
+ File.join(deploy_to, version_dir)
212
+ end
213
+
214
+ # Return the path identifying the +current+ symlink, used to identify the
215
+ # current release.
216
+ def current_path
217
+ File.join(deploy_to, current_dir)
218
+ end
219
+
220
+ # Return the path into which shared files should be stored.
221
+ def shared_path
222
+ File.join(deploy_to, shared_dir)
223
+ end
224
+
225
+ # Return the full path to the named release. If a release is not specified,
226
+ # +now+ is used (the time at which the configuration was created).
227
+ def release_path(release=now.strftime("%Y%m%d%H%M%S"))
228
+ File.join(releases_path, release)
229
+ end
230
+
231
+ def respond_to?(sym) #:nodoc:
232
+ @variables.has_key?(sym) || super
233
+ end
234
+
235
+ def method_missing(sym, *args, &block) #:nodoc:
236
+ if args.length == 0 && block.nil? && @variables.has_key?(sym)
237
+ self[sym]
238
+ else
239
+ super
240
+ end
241
+ end
242
+ end
243
+ end