capistrano-edge 2.5.6
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.rdoc +770 -0
- data/Manifest +104 -0
- data/README.rdoc +66 -0
- data/Rakefile +35 -0
- data/bin/cap +4 -0
- data/bin/capify +95 -0
- data/capistrano.gemspec +51 -0
- data/examples/sample.rb +14 -0
- data/lib/capistrano.rb +2 -0
- data/lib/capistrano/callback.rb +45 -0
- data/lib/capistrano/cli.rb +47 -0
- data/lib/capistrano/cli/execute.rb +84 -0
- data/lib/capistrano/cli/help.rb +125 -0
- data/lib/capistrano/cli/help.txt +75 -0
- data/lib/capistrano/cli/options.rb +224 -0
- data/lib/capistrano/cli/ui.rb +40 -0
- data/lib/capistrano/command.rb +283 -0
- data/lib/capistrano/configuration.rb +43 -0
- data/lib/capistrano/configuration/actions/file_transfer.rb +47 -0
- data/lib/capistrano/configuration/actions/inspect.rb +46 -0
- data/lib/capistrano/configuration/actions/invocation.rb +293 -0
- data/lib/capistrano/configuration/callbacks.rb +148 -0
- data/lib/capistrano/configuration/connections.rb +204 -0
- data/lib/capistrano/configuration/execution.rb +143 -0
- data/lib/capistrano/configuration/loading.rb +197 -0
- data/lib/capistrano/configuration/namespaces.rb +197 -0
- data/lib/capistrano/configuration/roles.rb +73 -0
- data/lib/capistrano/configuration/servers.rb +85 -0
- data/lib/capistrano/configuration/variables.rb +127 -0
- data/lib/capistrano/errors.rb +15 -0
- data/lib/capistrano/extensions.rb +57 -0
- data/lib/capistrano/logger.rb +59 -0
- data/lib/capistrano/processable.rb +53 -0
- data/lib/capistrano/recipes/compat.rb +32 -0
- data/lib/capistrano/recipes/deploy.rb +438 -0
- data/lib/capistrano/recipes/deploy/dependencies.rb +44 -0
- data/lib/capistrano/recipes/deploy/local_dependency.rb +54 -0
- data/lib/capistrano/recipes/deploy/remote_dependency.rb +105 -0
- data/lib/capistrano/recipes/deploy/scm.rb +19 -0
- data/lib/capistrano/recipes/deploy/scm/accurev.rb +169 -0
- data/lib/capistrano/recipes/deploy/scm/base.rb +196 -0
- data/lib/capistrano/recipes/deploy/scm/bzr.rb +83 -0
- data/lib/capistrano/recipes/deploy/scm/cvs.rb +152 -0
- data/lib/capistrano/recipes/deploy/scm/darcs.rb +85 -0
- data/lib/capistrano/recipes/deploy/scm/git.rb +274 -0
- data/lib/capistrano/recipes/deploy/scm/mercurial.rb +137 -0
- data/lib/capistrano/recipes/deploy/scm/none.rb +44 -0
- data/lib/capistrano/recipes/deploy/scm/perforce.rb +138 -0
- data/lib/capistrano/recipes/deploy/scm/subversion.rb +121 -0
- data/lib/capistrano/recipes/deploy/strategy.rb +19 -0
- data/lib/capistrano/recipes/deploy/strategy/base.rb +79 -0
- data/lib/capistrano/recipes/deploy/strategy/checkout.rb +20 -0
- data/lib/capistrano/recipes/deploy/strategy/copy.rb +210 -0
- data/lib/capistrano/recipes/deploy/strategy/export.rb +20 -0
- data/lib/capistrano/recipes/deploy/strategy/remote.rb +52 -0
- data/lib/capistrano/recipes/deploy/strategy/remote_cache.rb +56 -0
- data/lib/capistrano/recipes/deploy/templates/maintenance.rhtml +53 -0
- data/lib/capistrano/recipes/ext/rails-database-migrations.rb +50 -0
- data/lib/capistrano/recipes/ext/web-disable-enable.rb +40 -0
- data/lib/capistrano/recipes/standard.rb +37 -0
- data/lib/capistrano/recipes/templates/maintenance.rhtml +53 -0
- data/lib/capistrano/recipes/upgrade.rb +33 -0
- data/lib/capistrano/role.rb +102 -0
- data/lib/capistrano/server_definition.rb +56 -0
- data/lib/capistrano/shell.rb +260 -0
- data/lib/capistrano/ssh.rb +99 -0
- data/lib/capistrano/task_definition.rb +70 -0
- data/lib/capistrano/transfer.rb +216 -0
- data/lib/capistrano/version.rb +18 -0
- data/setup.rb +1346 -0
- data/test/cli/execute_test.rb +132 -0
- data/test/cli/help_test.rb +165 -0
- data/test/cli/options_test.rb +317 -0
- data/test/cli/ui_test.rb +28 -0
- data/test/cli_test.rb +17 -0
- data/test/command_test.rb +286 -0
- data/test/configuration/actions/file_transfer_test.rb +61 -0
- data/test/configuration/actions/inspect_test.rb +65 -0
- data/test/configuration/actions/invocation_test.rb +224 -0
- data/test/configuration/callbacks_test.rb +220 -0
- data/test/configuration/connections_test.rb +349 -0
- data/test/configuration/execution_test.rb +175 -0
- data/test/configuration/loading_test.rb +132 -0
- data/test/configuration/namespace_dsl_test.rb +311 -0
- data/test/configuration/roles_test.rb +144 -0
- data/test/configuration/servers_test.rb +121 -0
- data/test/configuration/variables_test.rb +184 -0
- data/test/configuration_test.rb +88 -0
- data/test/deploy/local_dependency_test.rb +76 -0
- data/test/deploy/remote_dependency_test.rb +114 -0
- data/test/deploy/scm/accurev_test.rb +23 -0
- data/test/deploy/scm/base_test.rb +55 -0
- data/test/deploy/scm/git_test.rb +184 -0
- data/test/deploy/scm/mercurial_test.rb +129 -0
- data/test/deploy/scm/none_test.rb +35 -0
- data/test/deploy/strategy/copy_test.rb +258 -0
- data/test/extensions_test.rb +69 -0
- data/test/fixtures/cli_integration.rb +5 -0
- data/test/fixtures/config.rb +5 -0
- data/test/fixtures/custom.rb +3 -0
- data/test/logger_test.rb +123 -0
- data/test/role_test.rb +11 -0
- data/test/server_definition_test.rb +121 -0
- data/test/shell_test.rb +90 -0
- data/test/ssh_test.rb +104 -0
- data/test/task_definition_test.rb +101 -0
- data/test/transfer_test.rb +160 -0
- data/test/utils.rb +38 -0
- metadata +321 -0
@@ -0,0 +1,50 @@
|
|
1
|
+
namespace :deploy do
|
2
|
+
|
3
|
+
desc <<-DESC
|
4
|
+
Run the migrate rake task. By default, it runs this in most recently \
|
5
|
+
deployed version of the app. However, you can specify a different release \
|
6
|
+
via the migrate_target variable, which must be one of :latest (for the \
|
7
|
+
default behavior), or :current (for the release indicated by the \
|
8
|
+
`current' symlink). Strings will work for those values instead of symbols, \
|
9
|
+
too. You can also specify additional environment variables to pass to rake \
|
10
|
+
via the migrate_env variable. Finally, you can specify the full path to the \
|
11
|
+
rake executable by setting the rake variable. The defaults are:
|
12
|
+
|
13
|
+
set :rake, "rake"
|
14
|
+
set :rails_env, "production"
|
15
|
+
set :migrate_env, ""
|
16
|
+
set :migrate_target, :latest
|
17
|
+
DESC
|
18
|
+
task :migrate, :roles => :db, :only => { :primary => true } do
|
19
|
+
rake = fetch(:rake, "rake")
|
20
|
+
rails_env = fetch(:rails_env, "production")
|
21
|
+
migrate_env = fetch(:migrate_env, "")
|
22
|
+
migrate_target = fetch(:migrate_target, :latest)
|
23
|
+
|
24
|
+
directory = case migrate_target.to_sym
|
25
|
+
when :current then current_path
|
26
|
+
when :latest then current_release
|
27
|
+
else raise ArgumentError, "unknown migration target #{migrate_target.inspect}"
|
28
|
+
end
|
29
|
+
|
30
|
+
run "cd #{directory}; #{rake} RAILS_ENV=#{rails_env} #{migrate_env} db:migrate"
|
31
|
+
end
|
32
|
+
|
33
|
+
desc <<-DESC
|
34
|
+
Deploy and run pending migrations. This will work similarly to the \
|
35
|
+
`deploy' task, but will also run any pending migrations (via the \
|
36
|
+
`deploy:migrate' task) prior to updating the symlink. Note that the \
|
37
|
+
update in this case it is not atomic, and transactions are not used, \
|
38
|
+
because migrations are not guaranteed to be reversible.
|
39
|
+
DESC
|
40
|
+
task :migrations do
|
41
|
+
set :migrate_target, :latest
|
42
|
+
update_code
|
43
|
+
migrate
|
44
|
+
symlink
|
45
|
+
restart
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
after('deploy:update_code', 'deploy:migrate')
|
@@ -0,0 +1,40 @@
|
|
1
|
+
namespace :web do
|
2
|
+
desc <<-DESC
|
3
|
+
Present a maintenance page to visitors. Disables your application's web \
|
4
|
+
interface by writing a "maintenance.html" file to each web server. The \
|
5
|
+
servers must be configured to detect the presence of this file, and if \
|
6
|
+
it is present, always display it instead of performing the request.
|
7
|
+
|
8
|
+
By default, the maintenance page will just say the site is down for \
|
9
|
+
"maintenance", and will be back "shortly", but you can customize the \
|
10
|
+
page by specifying the REASON and UNTIL environment variables:
|
11
|
+
|
12
|
+
$ cap deploy:web:disable \\
|
13
|
+
REASON="hardware upgrade" \\
|
14
|
+
UNTIL="12pm Central Time"
|
15
|
+
|
16
|
+
Further customization will require that you write your own task.
|
17
|
+
DESC
|
18
|
+
task :disable, :roles => :web, :except => { :no_release => true } do
|
19
|
+
require 'erb'
|
20
|
+
on_rollback { run "rm #{shared_path}/system/maintenance.html" }
|
21
|
+
|
22
|
+
reason = ENV['REASON']
|
23
|
+
deadline = ENV['UNTIL']
|
24
|
+
|
25
|
+
template = File.read(File.join(File.dirname(__FILE__), "templates", "maintenance.rhtml"))
|
26
|
+
result = ERB.new(template).result(binding)
|
27
|
+
|
28
|
+
put result, "#{shared_path}/system/maintenance.html", :mode => 0644
|
29
|
+
end
|
30
|
+
|
31
|
+
desc <<-DESC
|
32
|
+
Makes the application web-accessible again. Removes the \
|
33
|
+
"maintenance.html" page generated by deploy:web:disable, which (if your \
|
34
|
+
web servers are configured correctly) will make your application \
|
35
|
+
web-accessible again.
|
36
|
+
DESC
|
37
|
+
task :enable, :roles => :web, :except => { :no_release => true } do
|
38
|
+
run "rm #{shared_path}/system/maintenance.html"
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
desc <<-DESC
|
2
|
+
Invoke a single command on the remote servers. This is useful for performing \
|
3
|
+
one-off commands that may not require a full task to be written for them. \
|
4
|
+
Simply specify the command to execute via the COMMAND environment variable. \
|
5
|
+
To execute the command only on certain roles, specify the ROLES environment \
|
6
|
+
variable as a comma-delimited list of role names. Alternatively, you can \
|
7
|
+
specify the HOSTS environment variable as a comma-delimited list of hostnames \
|
8
|
+
to execute the task on those hosts, explicitly. Lastly, if you want to \
|
9
|
+
execute the command via sudo, specify a non-empty value for the SUDO \
|
10
|
+
environment variable.
|
11
|
+
|
12
|
+
Sample usage:
|
13
|
+
|
14
|
+
$ cap COMMAND=uptime HOSTS=foo.capistano.test invoke
|
15
|
+
$ cap ROLES=app,web SUDO=1 COMMAND="tail -f /var/log/messages" invoke
|
16
|
+
DESC
|
17
|
+
task :invoke do
|
18
|
+
command = ENV["COMMAND"] || ""
|
19
|
+
abort "Please specify a command to execute on the remote servers (via the COMMAND environment variable)" if command.empty?
|
20
|
+
method = ENV["SUDO"] ? :sudo : :run
|
21
|
+
invoke_command(command, :via => method)
|
22
|
+
end
|
23
|
+
|
24
|
+
desc <<-DESC
|
25
|
+
Begin an interactive Capistrano session. This gives you an interactive \
|
26
|
+
terminal from which to execute tasks and commands on all of your servers. \
|
27
|
+
(This is still an experimental feature, and is subject to change without \
|
28
|
+
notice!)
|
29
|
+
|
30
|
+
Sample usage:
|
31
|
+
|
32
|
+
$ cap shell
|
33
|
+
DESC
|
34
|
+
task :shell do
|
35
|
+
require 'capistrano/shell'
|
36
|
+
Capistrano::Shell.run(self)
|
37
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
|
2
|
+
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
3
|
+
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
4
|
+
|
5
|
+
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
6
|
+
|
7
|
+
<head>
|
8
|
+
<meta http-equiv="content-type" content="text/html;charset=UTF-8" />
|
9
|
+
<title>System down for maintenance</title>
|
10
|
+
|
11
|
+
<style type="text/css">
|
12
|
+
div.outer {
|
13
|
+
position: absolute;
|
14
|
+
left: 50%;
|
15
|
+
top: 50%;
|
16
|
+
width: 500px;
|
17
|
+
height: 300px;
|
18
|
+
margin-left: -260px;
|
19
|
+
margin-top: -150px;
|
20
|
+
}
|
21
|
+
|
22
|
+
.DialogBody {
|
23
|
+
margin: 0;
|
24
|
+
padding: 10px;
|
25
|
+
text-align: left;
|
26
|
+
border: 1px solid #ccc;
|
27
|
+
border-right: 1px solid #999;
|
28
|
+
border-bottom: 1px solid #999;
|
29
|
+
background-color: #fff;
|
30
|
+
}
|
31
|
+
|
32
|
+
body { background-color: #fff; }
|
33
|
+
</style>
|
34
|
+
</head>
|
35
|
+
|
36
|
+
<body>
|
37
|
+
|
38
|
+
<div class="outer">
|
39
|
+
<div class="DialogBody" style="text-align: center;">
|
40
|
+
<div style="text-align: center; width: 200px; margin: 0 auto;">
|
41
|
+
<p style="color: red; font-size: 16px; line-height: 20px;">
|
42
|
+
The system is down for <%= reason ? reason : "maintenance" %>
|
43
|
+
as of <%= Time.now.strftime("%H:%M %Z") %>.
|
44
|
+
</p>
|
45
|
+
<p style="color: #666;">
|
46
|
+
It'll be back <%= deadline ? deadline : "shortly" %>.
|
47
|
+
</p>
|
48
|
+
</div>
|
49
|
+
</div>
|
50
|
+
</div>
|
51
|
+
|
52
|
+
</body>
|
53
|
+
</html>
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# Tasks to aid the migration of an established Capistrano 1.x installation to
|
2
|
+
# Capistrano 2.x.
|
3
|
+
|
4
|
+
namespace :upgrade do
|
5
|
+
desc <<-DESC
|
6
|
+
Migrate from the revisions log to REVISION. Capistrano 1.x recorded each \
|
7
|
+
deployment to a revisions.log file. Capistrano 2.x is cleaner, and just \
|
8
|
+
puts a REVISION file in the root of the deployed revision. This task \
|
9
|
+
migrates from the revisions.log used in Capistrano 1.x, to the REVISION \
|
10
|
+
tag file used in Capistrano 2.x. It is non-destructive and may be safely \
|
11
|
+
run any number of times.
|
12
|
+
DESC
|
13
|
+
task :revisions, :except => { :no_release => true } do
|
14
|
+
revisions = capture("cat #{deploy_to}/revisions.log")
|
15
|
+
|
16
|
+
mapping = {}
|
17
|
+
revisions.each do |line|
|
18
|
+
revision, directory = line.chomp.split[-2,2]
|
19
|
+
mapping[directory] = revision
|
20
|
+
end
|
21
|
+
|
22
|
+
commands = mapping.keys.map do |directory|
|
23
|
+
"echo '.'; test -d #{directory} && echo '#{mapping[directory]}' > #{directory}/REVISION"
|
24
|
+
end
|
25
|
+
|
26
|
+
command = commands.join(";")
|
27
|
+
|
28
|
+
run "cd #{releases_path}; #{command}; true" do |ch, stream, out|
|
29
|
+
STDOUT.print(".")
|
30
|
+
STDOUT.flush
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
module Capistrano
|
2
|
+
class Role
|
3
|
+
include Enumerable
|
4
|
+
|
5
|
+
def initialize(*list)
|
6
|
+
@static_servers = []
|
7
|
+
@dynamic_servers = []
|
8
|
+
push(*list)
|
9
|
+
end
|
10
|
+
|
11
|
+
def each(&block)
|
12
|
+
servers.each &block
|
13
|
+
end
|
14
|
+
|
15
|
+
def push(*list)
|
16
|
+
options = list.last.is_a?(Hash) ? list.pop : {}
|
17
|
+
list.each do |item|
|
18
|
+
if item.respond_to?(:call)
|
19
|
+
@dynamic_servers << DynamicServerList.new(item, options)
|
20
|
+
else
|
21
|
+
@static_servers << self.class.wrap_server(item, options)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
alias_method :<<, :push
|
26
|
+
|
27
|
+
def servers
|
28
|
+
@static_servers + dynamic_servers
|
29
|
+
end
|
30
|
+
alias_method :to_ary, :servers
|
31
|
+
|
32
|
+
def empty?
|
33
|
+
servers.empty?
|
34
|
+
end
|
35
|
+
|
36
|
+
def clear
|
37
|
+
@dynamic_servers.clear
|
38
|
+
@static_servers.clear
|
39
|
+
end
|
40
|
+
|
41
|
+
def include?(server)
|
42
|
+
servers.include?(server)
|
43
|
+
end
|
44
|
+
|
45
|
+
protected
|
46
|
+
|
47
|
+
# This is the combination of a block, a hash of options, and a cached value.
|
48
|
+
class DynamicServerList
|
49
|
+
def initialize (block, options)
|
50
|
+
@block = block
|
51
|
+
@options = options
|
52
|
+
@cached = []
|
53
|
+
@is_cached = false
|
54
|
+
end
|
55
|
+
|
56
|
+
# Convert to a list of ServerDefinitions
|
57
|
+
def to_ary
|
58
|
+
unless @is_cached
|
59
|
+
@cached = Role::wrap_list(@block.call(@options), @options)
|
60
|
+
@is_cached = true
|
61
|
+
end
|
62
|
+
@cached
|
63
|
+
end
|
64
|
+
|
65
|
+
# Clear the cached value
|
66
|
+
def reset!
|
67
|
+
@cached.clear
|
68
|
+
@is_cached = false
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Attribute reader for the cached results of executing the blocks in turn
|
73
|
+
def dynamic_servers
|
74
|
+
@dynamic_servers.inject([]) { |list, item| list.concat item }
|
75
|
+
end
|
76
|
+
|
77
|
+
# Wraps a string in a ServerDefinition, if it isn't already.
|
78
|
+
# This and wrap_list should probably go in ServerDefinition in some form.
|
79
|
+
def self.wrap_server (item, options)
|
80
|
+
item.is_a?(ServerDefinition) ? item : ServerDefinition.new(item, options)
|
81
|
+
end
|
82
|
+
|
83
|
+
# Turns a list, or something resembling a list, into a properly-formatted
|
84
|
+
# ServerDefinition list. Keep an eye on this one -- it's entirely too
|
85
|
+
# magical for its own good. In particular, if ServerDefinition ever inherits
|
86
|
+
# from Array, this will break.
|
87
|
+
def self.wrap_list (*list)
|
88
|
+
options = list.last.is_a?(Hash) ? list.pop : {}
|
89
|
+
if list.length == 1
|
90
|
+
if list.first.nil?
|
91
|
+
return []
|
92
|
+
elsif list.first.is_a?(Array)
|
93
|
+
list = list.first
|
94
|
+
end
|
95
|
+
end
|
96
|
+
options.merge! list.pop if list.last.is_a?(Hash)
|
97
|
+
list.map do |item|
|
98
|
+
self.wrap_server item, options
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module Capistrano
|
2
|
+
class ServerDefinition
|
3
|
+
include Comparable
|
4
|
+
|
5
|
+
attr_reader :host
|
6
|
+
attr_reader :user
|
7
|
+
attr_reader :port
|
8
|
+
attr_reader :options
|
9
|
+
|
10
|
+
# The default user name to use when a user name is not explicitly provided
|
11
|
+
def self.default_user
|
12
|
+
ENV['USER'] || ENV['USERNAME'] || "not-specified"
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(string, options={})
|
16
|
+
@user, @host, @port = string.match(/^(?:([^;,:=]+)@|)(.*?)(?::(\d+)|)$/)[1,3]
|
17
|
+
|
18
|
+
@options = options.dup
|
19
|
+
user_opt, port_opt = @options.delete(:user), @options.delete(:port)
|
20
|
+
|
21
|
+
@user ||= user_opt
|
22
|
+
@port ||= port_opt
|
23
|
+
|
24
|
+
@port = @port.to_i if @port
|
25
|
+
end
|
26
|
+
|
27
|
+
def <=>(server)
|
28
|
+
[host, port, user] <=> [server.host, server.port, server.user]
|
29
|
+
end
|
30
|
+
|
31
|
+
# Redefined, so that Array#uniq will work to remove duplicate server
|
32
|
+
# definitions, based solely on their host names.
|
33
|
+
def eql?(server)
|
34
|
+
host == server.host &&
|
35
|
+
user == server.user &&
|
36
|
+
port == server.port
|
37
|
+
end
|
38
|
+
|
39
|
+
alias :== :eql?
|
40
|
+
|
41
|
+
# Redefined, so that Array#uniq will work to remove duplicate server
|
42
|
+
# definitions, based on their connection information.
|
43
|
+
def hash
|
44
|
+
@hash ||= [host, user, port].hash
|
45
|
+
end
|
46
|
+
|
47
|
+
def to_s
|
48
|
+
@to_s ||= begin
|
49
|
+
s = host
|
50
|
+
s = "#{user}@#{s}" if user
|
51
|
+
s = "#{s}:#{port}" if port && port != 22
|
52
|
+
s
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,260 @@
|
|
1
|
+
require 'thread'
|
2
|
+
require 'capistrano/processable'
|
3
|
+
|
4
|
+
module Capistrano
|
5
|
+
# The Capistrano::Shell class is the guts of the "shell" task. It implements
|
6
|
+
# an interactive REPL interface that users can employ to execute tasks and
|
7
|
+
# commands. It makes for a GREAT way to monitor systems, and perform quick
|
8
|
+
# maintenance on one or more machines.
|
9
|
+
class Shell
|
10
|
+
include Processable
|
11
|
+
|
12
|
+
# A Readline replacement for platforms where readline is either
|
13
|
+
# unavailable, or has not been installed.
|
14
|
+
class ReadlineFallback #:nodoc:
|
15
|
+
HISTORY = []
|
16
|
+
|
17
|
+
def self.readline(prompt)
|
18
|
+
STDOUT.print(prompt)
|
19
|
+
STDOUT.flush
|
20
|
+
STDIN.gets
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# The configuration instance employed by this shell
|
25
|
+
attr_reader :configuration
|
26
|
+
|
27
|
+
# Instantiate a new shell and begin executing it immediately.
|
28
|
+
def self.run(config)
|
29
|
+
new(config).run!
|
30
|
+
end
|
31
|
+
|
32
|
+
# Instantiate a new shell
|
33
|
+
def initialize(config)
|
34
|
+
@configuration = config
|
35
|
+
end
|
36
|
+
|
37
|
+
# Start the shell running. This method will block until the shell
|
38
|
+
# terminates.
|
39
|
+
def run!
|
40
|
+
setup
|
41
|
+
|
42
|
+
puts <<-INTRO
|
43
|
+
====================================================================
|
44
|
+
Welcome to the interactive Capistrano shell! This is an experimental
|
45
|
+
feature, and is liable to change in future releases. Type 'help' for
|
46
|
+
a summary of how to use the shell.
|
47
|
+
--------------------------------------------------------------------
|
48
|
+
INTRO
|
49
|
+
|
50
|
+
loop do
|
51
|
+
break if !read_and_execute
|
52
|
+
end
|
53
|
+
|
54
|
+
@bgthread.kill
|
55
|
+
end
|
56
|
+
|
57
|
+
def read_and_execute
|
58
|
+
command = read_line
|
59
|
+
|
60
|
+
case command
|
61
|
+
when "?", "help" then help
|
62
|
+
when "quit", "exit" then
|
63
|
+
puts "exiting"
|
64
|
+
return false
|
65
|
+
when /^set -(\w)\s*(\S+)/
|
66
|
+
set_option($1, $2)
|
67
|
+
when /^(?:(with|on)\s*(\S+))?\s*(\S.*)?/i
|
68
|
+
process_command($1, $2, $3)
|
69
|
+
else
|
70
|
+
raise "eh?"
|
71
|
+
end
|
72
|
+
|
73
|
+
return true
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
# Present the prompt and read a single line from the console. It also
|
79
|
+
# detects ^D and returns "exit" in that case. Adds the input to the
|
80
|
+
# history, unless the input is empty. Loops repeatedly until a non-empty
|
81
|
+
# line is input.
|
82
|
+
def read_line
|
83
|
+
loop do
|
84
|
+
command = reader.readline("cap> ")
|
85
|
+
|
86
|
+
if command.nil?
|
87
|
+
command = "exit"
|
88
|
+
puts(command)
|
89
|
+
else
|
90
|
+
command.strip!
|
91
|
+
end
|
92
|
+
|
93
|
+
unless command.empty?
|
94
|
+
reader::HISTORY << command
|
95
|
+
return command
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# Display a verbose help message.
|
101
|
+
def help
|
102
|
+
puts <<-HELP
|
103
|
+
--- HELP! ---------------------------------------------------
|
104
|
+
"Get me out of this thing. I just want to quit."
|
105
|
+
-> Easy enough. Just type "exit", or "quit". Or press ctrl-D.
|
106
|
+
|
107
|
+
"I want to execute a command on all servers."
|
108
|
+
-> Just type the command, and press enter. It will be passed,
|
109
|
+
verbatim, to all defined servers.
|
110
|
+
|
111
|
+
"What if I only want it to execute on a subset of them?"
|
112
|
+
-> No problem, just specify the list of servers, separated by
|
113
|
+
commas, before the command, with the `on' keyword:
|
114
|
+
|
115
|
+
cap> on app1.foo.com,app2.foo.com echo ping
|
116
|
+
|
117
|
+
"Nice, but can I specify the servers by role?"
|
118
|
+
-> You sure can. Just use the `with' keyword, followed by the
|
119
|
+
comma-delimited list of role names:
|
120
|
+
|
121
|
+
cap> with app,db echo ping
|
122
|
+
|
123
|
+
"Can I execute a Capistrano task from within this shell?"
|
124
|
+
-> Yup. Just prefix the task with an exclamation mark:
|
125
|
+
|
126
|
+
cap> !deploy
|
127
|
+
HELP
|
128
|
+
end
|
129
|
+
|
130
|
+
# Determine which servers the given task requires a connection to, and
|
131
|
+
# establish connections to them if necessary. Return the list of
|
132
|
+
# servers (names).
|
133
|
+
def connect(task)
|
134
|
+
servers = configuration.find_servers_for_task(task)
|
135
|
+
needing_connections = servers - configuration.sessions.keys
|
136
|
+
unless needing_connections.empty?
|
137
|
+
puts "[establishing connection(s) to #{needing_connections.join(', ')}]"
|
138
|
+
configuration.establish_connections_to(needing_connections)
|
139
|
+
end
|
140
|
+
servers
|
141
|
+
end
|
142
|
+
|
143
|
+
# Execute the given command. If the command is prefixed by an exclamation
|
144
|
+
# mark, it is assumed to refer to another capistrano task, which will
|
145
|
+
# be invoked. Otherwise, it is executed as a command on all associated
|
146
|
+
# servers.
|
147
|
+
def exec(command)
|
148
|
+
@mutex.synchronize do
|
149
|
+
if command[0] == ?!
|
150
|
+
exec_tasks(command[1..-1].split)
|
151
|
+
else
|
152
|
+
servers = connect(configuration.current_task)
|
153
|
+
exec_command(command, servers)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
ensure
|
157
|
+
STDOUT.flush
|
158
|
+
end
|
159
|
+
|
160
|
+
# Given an array of task names, invoke them in sequence.
|
161
|
+
def exec_tasks(list)
|
162
|
+
list.each do |task_name|
|
163
|
+
task = configuration.find_task(task_name)
|
164
|
+
raise Capistrano::NoSuchTaskError, "no such task `#{task_name}'" unless task
|
165
|
+
connect(task)
|
166
|
+
configuration.execute_task(task)
|
167
|
+
end
|
168
|
+
rescue Capistrano::NoMatchingServersError, Capistrano::NoSuchTaskError => error
|
169
|
+
warn "error: #{error.message}"
|
170
|
+
end
|
171
|
+
|
172
|
+
# Execute a command on the given list of servers.
|
173
|
+
def exec_command(command, servers)
|
174
|
+
command = command.gsub(/\bsudo\b/, "sudo -p '#{configuration.sudo_prompt}'")
|
175
|
+
processor = configuration.sudo_behavior_callback(Configuration.default_io_proc)
|
176
|
+
sessions = servers.map { |server| configuration.sessions[server] }
|
177
|
+
options = configuration.add_default_command_options({})
|
178
|
+
cmd = Command.new(command, sessions, options.merge(:logger => configuration.logger), &processor)
|
179
|
+
previous = trap("INT") { cmd.stop! }
|
180
|
+
cmd.process!
|
181
|
+
rescue Capistrano::Error => error
|
182
|
+
warn "error: #{error.message}"
|
183
|
+
ensure
|
184
|
+
trap("INT", previous)
|
185
|
+
end
|
186
|
+
|
187
|
+
# Return the object that will be used to query input from the console.
|
188
|
+
# The returned object will quack (more or less) like Readline.
|
189
|
+
def reader
|
190
|
+
@reader ||= begin
|
191
|
+
require 'readline'
|
192
|
+
Readline
|
193
|
+
rescue LoadError
|
194
|
+
ReadlineFallback
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
# Prepare every little thing for the shell. Starts the background
|
199
|
+
# thread and generally gets things ready for the REPL.
|
200
|
+
def setup
|
201
|
+
configuration.logger.level = Capistrano::Logger::INFO
|
202
|
+
|
203
|
+
@mutex = Mutex.new
|
204
|
+
@bgthread = Thread.new do
|
205
|
+
loop do
|
206
|
+
@mutex.synchronize { process_iteration(0.1) }
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
# Set the given option to +value+.
|
212
|
+
def set_option(opt, value)
|
213
|
+
case opt
|
214
|
+
when "v" then
|
215
|
+
puts "setting log verbosity to #{value.to_i}"
|
216
|
+
configuration.logger.level = value.to_i
|
217
|
+
when "o" then
|
218
|
+
case value
|
219
|
+
when "vi" then
|
220
|
+
puts "using vi edit mode"
|
221
|
+
reader.vi_editing_mode
|
222
|
+
when "emacs" then
|
223
|
+
puts "using emacs edit mode"
|
224
|
+
reader.emacs_editing_mode
|
225
|
+
else
|
226
|
+
puts "unknown -o option #{value.inspect}"
|
227
|
+
end
|
228
|
+
else
|
229
|
+
puts "unknown setting #{opt.inspect}"
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
# Process a command. Interprets the scope_type (must be nil, "with", or
|
234
|
+
# "on") and the command. If no command is given, then the scope is made
|
235
|
+
# effective for all subsequent commands. If the scope value is "all",
|
236
|
+
# then the scope is unrestricted.
|
237
|
+
def process_command(scope_type, scope_value, command)
|
238
|
+
env_var = case scope_type
|
239
|
+
when "with" then "ROLES"
|
240
|
+
when "on" then "HOSTS"
|
241
|
+
end
|
242
|
+
|
243
|
+
old_var, ENV[env_var] = ENV[env_var], (scope_value == "all" ? nil : scope_value) if env_var
|
244
|
+
if command
|
245
|
+
begin
|
246
|
+
exec(command)
|
247
|
+
ensure
|
248
|
+
ENV[env_var] = old_var if env_var
|
249
|
+
end
|
250
|
+
else
|
251
|
+
puts "scoping #{scope_type} #{scope_value}"
|
252
|
+
end
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
# All open sessions, needed to satisfy the Command::Processable include
|
257
|
+
def sessions
|
258
|
+
configuration.sessions.values
|
259
|
+
end
|
260
|
+
end
|