capistrano 1.1.0

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.
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