capistrano-edge 2.5.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/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
|