minmb-capistrano 2.15.4
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +10 -0
- data/.travis.yml +7 -0
- data/CHANGELOG +1170 -0
- data/Gemfile +13 -0
- data/README.md +94 -0
- data/Rakefile +11 -0
- data/bin/cap +4 -0
- data/bin/capify +92 -0
- data/capistrano.gemspec +40 -0
- data/lib/capistrano.rb +5 -0
- data/lib/capistrano/callback.rb +45 -0
- data/lib/capistrano/cli.rb +47 -0
- data/lib/capistrano/cli/execute.rb +85 -0
- data/lib/capistrano/cli/help.rb +125 -0
- data/lib/capistrano/cli/help.txt +81 -0
- data/lib/capistrano/cli/options.rb +243 -0
- data/lib/capistrano/cli/ui.rb +40 -0
- data/lib/capistrano/command.rb +303 -0
- data/lib/capistrano/configuration.rb +57 -0
- data/lib/capistrano/configuration/actions/file_transfer.rb +50 -0
- data/lib/capistrano/configuration/actions/inspect.rb +46 -0
- data/lib/capistrano/configuration/actions/invocation.rb +329 -0
- data/lib/capistrano/configuration/alias_task.rb +26 -0
- data/lib/capistrano/configuration/callbacks.rb +147 -0
- data/lib/capistrano/configuration/connections.rb +237 -0
- data/lib/capistrano/configuration/execution.rb +142 -0
- data/lib/capistrano/configuration/loading.rb +205 -0
- data/lib/capistrano/configuration/log_formatters.rb +75 -0
- data/lib/capistrano/configuration/namespaces.rb +223 -0
- data/lib/capistrano/configuration/roles.rb +77 -0
- data/lib/capistrano/configuration/servers.rb +116 -0
- data/lib/capistrano/configuration/variables.rb +127 -0
- data/lib/capistrano/errors.rb +19 -0
- data/lib/capistrano/ext/multistage.rb +64 -0
- data/lib/capistrano/ext/string.rb +5 -0
- data/lib/capistrano/extensions.rb +57 -0
- data/lib/capistrano/fix_rake_deprecated_dsl.rb +8 -0
- data/lib/capistrano/logger.rb +166 -0
- data/lib/capistrano/processable.rb +57 -0
- data/lib/capistrano/recipes/compat.rb +32 -0
- data/lib/capistrano/recipes/deploy.rb +625 -0
- data/lib/capistrano/recipes/deploy/assets.rb +201 -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 +117 -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 +200 -0
- data/lib/capistrano/recipes/deploy/scm/bzr.rb +86 -0
- data/lib/capistrano/recipes/deploy/scm/cvs.rb +153 -0
- data/lib/capistrano/recipes/deploy/scm/darcs.rb +96 -0
- data/lib/capistrano/recipes/deploy/scm/git.rb +293 -0
- data/lib/capistrano/recipes/deploy/scm/mercurial.rb +137 -0
- data/lib/capistrano/recipes/deploy/scm/none.rb +55 -0
- data/lib/capistrano/recipes/deploy/scm/perforce.rb +152 -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 +92 -0
- data/lib/capistrano/recipes/deploy/strategy/checkout.rb +20 -0
- data/lib/capistrano/recipes/deploy/strategy/copy.rb +338 -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 +57 -0
- data/lib/capistrano/recipes/deploy/strategy/unshared_remote_cache.rb +21 -0
- data/lib/capistrano/recipes/standard.rb +37 -0
- data/lib/capistrano/recipes/templates/maintenance.rhtml +53 -0
- data/lib/capistrano/role.rb +102 -0
- data/lib/capistrano/server_definition.rb +56 -0
- data/lib/capistrano/shell.rb +265 -0
- data/lib/capistrano/ssh.rb +95 -0
- data/lib/capistrano/task_definition.rb +77 -0
- data/lib/capistrano/transfer.rb +218 -0
- data/lib/capistrano/version.rb +11 -0
- data/test/cli/execute_test.rb +132 -0
- data/test/cli/help_test.rb +165 -0
- data/test/cli/options_test.rb +329 -0
- data/test/cli/ui_test.rb +28 -0
- data/test/cli_test.rb +17 -0
- data/test/command_test.rb +322 -0
- data/test/configuration/actions/file_transfer_test.rb +61 -0
- data/test/configuration/actions/inspect_test.rb +76 -0
- data/test/configuration/actions/invocation_test.rb +288 -0
- data/test/configuration/alias_task_test.rb +118 -0
- data/test/configuration/callbacks_test.rb +201 -0
- data/test/configuration/connections_test.rb +439 -0
- data/test/configuration/execution_test.rb +175 -0
- data/test/configuration/loading_test.rb +148 -0
- data/test/configuration/namespace_dsl_test.rb +332 -0
- data/test/configuration/roles_test.rb +157 -0
- data/test/configuration/servers_test.rb +183 -0
- data/test/configuration/variables_test.rb +190 -0
- data/test/configuration_test.rb +77 -0
- data/test/deploy/local_dependency_test.rb +76 -0
- data/test/deploy/remote_dependency_test.rb +146 -0
- data/test/deploy/scm/accurev_test.rb +23 -0
- data/test/deploy/scm/base_test.rb +55 -0
- data/test/deploy/scm/bzr_test.rb +51 -0
- data/test/deploy/scm/darcs_test.rb +37 -0
- data/test/deploy/scm/git_test.rb +221 -0
- data/test/deploy/scm/mercurial_test.rb +134 -0
- data/test/deploy/scm/none_test.rb +35 -0
- data/test/deploy/scm/perforce_test.rb +23 -0
- data/test/deploy/scm/subversion_test.rb +40 -0
- data/test/deploy/strategy/copy_test.rb +360 -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_formatting_test.rb +149 -0
- data/test/logger_test.rb +134 -0
- data/test/recipes_test.rb +25 -0
- data/test/role_test.rb +11 -0
- data/test/server_definition_test.rb +121 -0
- data/test/shell_test.rb +96 -0
- data/test/ssh_test.rb +113 -0
- data/test/task_definition_test.rb +117 -0
- data/test/transfer_test.rb +168 -0
- data/test/utils.rb +37 -0
- metadata +316 -0
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'capistrano/recipes/deploy/strategy/remote'
|
2
|
+
|
3
|
+
module Capistrano
|
4
|
+
module Deploy
|
5
|
+
module Strategy
|
6
|
+
|
7
|
+
# Implements the deployment strategy which does an SCM export on each
|
8
|
+
# target host.
|
9
|
+
class Export < Remote
|
10
|
+
protected
|
11
|
+
|
12
|
+
# Returns the SCM's export command for the revision to deploy.
|
13
|
+
def command
|
14
|
+
@command ||= source.export(revision, configuration[:release_path])
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'capistrano/recipes/deploy/strategy/base'
|
2
|
+
|
3
|
+
module Capistrano
|
4
|
+
module Deploy
|
5
|
+
module Strategy
|
6
|
+
|
7
|
+
# An abstract superclass, which forms the base for all deployment
|
8
|
+
# strategies which work by grabbing the code from the repository directly
|
9
|
+
# from remote host. This includes deploying by checkout (the default),
|
10
|
+
# and deploying by export.
|
11
|
+
class Remote < Base
|
12
|
+
# Executes the SCM command for this strategy and writes the REVISION
|
13
|
+
# mark file to each host.
|
14
|
+
def deploy!
|
15
|
+
scm_run "#{command} && #{mark}"
|
16
|
+
end
|
17
|
+
|
18
|
+
def check!
|
19
|
+
super.check do |d|
|
20
|
+
d.remote.command(source.command)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
protected
|
25
|
+
|
26
|
+
# Runs the given command, filtering output back through the
|
27
|
+
# #handle_data filter of the SCM implementation.
|
28
|
+
def scm_run(command)
|
29
|
+
run(command) do |ch,stream,text|
|
30
|
+
ch[:state] ||= { :channel => ch }
|
31
|
+
output = source.handle_data(ch[:state], stream, text)
|
32
|
+
ch.send_data(output) if output
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# An abstract method which must be overridden in subclasses, to
|
37
|
+
# return the actual SCM command(s) which must be executed on each
|
38
|
+
# target host in order to perform the deployment.
|
39
|
+
def command
|
40
|
+
raise NotImplementedError, "`command' is not implemented by #{self.class.name}"
|
41
|
+
end
|
42
|
+
|
43
|
+
# Returns the command which will write the identifier of the
|
44
|
+
# revision being deployed to the REVISION file on each host.
|
45
|
+
def mark
|
46
|
+
"(echo #{revision} > #{configuration[:release_path]}/REVISION)"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'capistrano/recipes/deploy/strategy/remote'
|
2
|
+
|
3
|
+
module Capistrano
|
4
|
+
module Deploy
|
5
|
+
module Strategy
|
6
|
+
|
7
|
+
# Implements the deployment strategy that keeps a cached checkout of
|
8
|
+
# the source code on each remote server. Each deploy simply updates the
|
9
|
+
# cached checkout, and then does a copy from the cached copy to the
|
10
|
+
# final deployment location.
|
11
|
+
class RemoteCache < Remote
|
12
|
+
# Executes the SCM command for this strategy and writes the REVISION
|
13
|
+
# mark file to each host.
|
14
|
+
def deploy!
|
15
|
+
update_repository_cache
|
16
|
+
copy_repository_cache
|
17
|
+
end
|
18
|
+
|
19
|
+
def check!
|
20
|
+
super.check do |d|
|
21
|
+
d.remote.command("rsync") unless copy_exclude.empty?
|
22
|
+
d.remote.writable(shared_path)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def repository_cache
|
29
|
+
File.join(shared_path, configuration[:repository_cache] || "cached-copy")
|
30
|
+
end
|
31
|
+
|
32
|
+
def update_repository_cache
|
33
|
+
logger.trace "updating the cached checkout on all servers"
|
34
|
+
command = "if [ -d #{repository_cache} ]; then " +
|
35
|
+
"#{source.sync(revision, repository_cache)}; " +
|
36
|
+
"else #{source.checkout(revision, repository_cache)}; fi"
|
37
|
+
scm_run(command)
|
38
|
+
end
|
39
|
+
|
40
|
+
def copy_repository_cache
|
41
|
+
logger.trace "copying the cached version to #{configuration[:release_path]}"
|
42
|
+
if copy_exclude.empty?
|
43
|
+
run "cp -RPp #{repository_cache} #{configuration[:release_path]} && #{mark}"
|
44
|
+
else
|
45
|
+
exclusions = copy_exclude.map { |e| "--exclude=\"#{e}\"" }.join(' ')
|
46
|
+
run "rsync -lrpt #{exclusions} #{repository_cache}/ #{configuration[:release_path]} && #{mark}"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def copy_exclude
|
51
|
+
@copy_exclude ||= Array(configuration.fetch(:copy_exclude, []))
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'capistrano/recipes/deploy/strategy/remote_cache'
|
2
|
+
|
3
|
+
module Capistrano
|
4
|
+
module Deploy
|
5
|
+
module Strategy
|
6
|
+
class UnsharedRemoteCache < RemoteCache
|
7
|
+
def check!
|
8
|
+
super.check do |d|
|
9
|
+
d.remote.writable(repository_cache)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def repository_cache
|
16
|
+
configuration[:repository_cache]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
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,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,265 @@
|
|
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 /^set :(.*)\s+(.*)/
|
68
|
+
configuration.set($1.to_sym, $2)
|
69
|
+
puts "updated :#{$1} to #{$2}"
|
70
|
+
when /^(?:(with|on)\s*(\S+))?\s*(\S.*)?/i
|
71
|
+
process_command($1, $2, $3)
|
72
|
+
else
|
73
|
+
raise "eh?"
|
74
|
+
end
|
75
|
+
|
76
|
+
return true
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
# Present the prompt and read a single line from the console. It also
|
82
|
+
# detects ^D and returns "exit" in that case. Adds the input to the
|
83
|
+
# history, unless the input is empty. Loops repeatedly until a non-empty
|
84
|
+
# line is input.
|
85
|
+
def read_line
|
86
|
+
loop do
|
87
|
+
command = reader.readline("cap> ")
|
88
|
+
|
89
|
+
if command.nil?
|
90
|
+
command = "exit"
|
91
|
+
puts(command)
|
92
|
+
else
|
93
|
+
command.strip!
|
94
|
+
end
|
95
|
+
|
96
|
+
unless command.empty?
|
97
|
+
reader::HISTORY << command
|
98
|
+
return command
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# Display a verbose help message.
|
104
|
+
def help
|
105
|
+
puts <<-HELP
|
106
|
+
--- HELP! ---------------------------------------------------
|
107
|
+
"Get me out of this thing. I just want to quit."
|
108
|
+
-> Easy enough. Just type "exit", or "quit". Or press ctrl-D.
|
109
|
+
|
110
|
+
"I want to execute a command on all servers."
|
111
|
+
-> Just type the command, and press enter. It will be passed,
|
112
|
+
verbatim, to all defined servers.
|
113
|
+
|
114
|
+
"What if I only want it to execute on a subset of them?"
|
115
|
+
-> No problem, just specify the list of servers, separated by
|
116
|
+
commas, before the command, with the `on' keyword:
|
117
|
+
|
118
|
+
cap> on app1.foo.com,app2.foo.com echo ping
|
119
|
+
|
120
|
+
"Nice, but can I specify the servers by role?"
|
121
|
+
-> You sure can. Just use the `with' keyword, followed by the
|
122
|
+
comma-delimited list of role names:
|
123
|
+
|
124
|
+
cap> with app,db echo ping
|
125
|
+
|
126
|
+
"Can I execute a Capistrano task from within this shell?"
|
127
|
+
-> Yup. Just prefix the task with an exclamation mark:
|
128
|
+
|
129
|
+
cap> !deploy
|
130
|
+
HELP
|
131
|
+
end
|
132
|
+
|
133
|
+
# Determine which servers the given task requires a connection to, and
|
134
|
+
# establish connections to them if necessary. Return the list of
|
135
|
+
# servers (names).
|
136
|
+
def connect(task)
|
137
|
+
servers = configuration.find_servers_for_task(task)
|
138
|
+
needing_connections = servers - configuration.sessions.keys
|
139
|
+
unless needing_connections.empty?
|
140
|
+
puts "[establishing connection(s) to #{needing_connections.join(', ')}]"
|
141
|
+
configuration.establish_connections_to(needing_connections)
|
142
|
+
end
|
143
|
+
servers
|
144
|
+
end
|
145
|
+
|
146
|
+
# Execute the given command. If the command is prefixed by an exclamation
|
147
|
+
# mark, it is assumed to refer to another capistrano task, which will
|
148
|
+
# be invoked. Otherwise, it is executed as a command on all associated
|
149
|
+
# servers.
|
150
|
+
def exec(command)
|
151
|
+
@mutex.synchronize do
|
152
|
+
if command[0] == ?!
|
153
|
+
exec_tasks(command[1..-1].split)
|
154
|
+
else
|
155
|
+
servers = connect(configuration.current_task)
|
156
|
+
exec_command(command, servers)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
ensure
|
160
|
+
STDOUT.flush
|
161
|
+
end
|
162
|
+
|
163
|
+
# Given an array of task names, invoke them in sequence.
|
164
|
+
def exec_tasks(list)
|
165
|
+
list.each do |task_name|
|
166
|
+
task = configuration.find_task(task_name)
|
167
|
+
raise Capistrano::NoSuchTaskError, "no such task `#{task_name}'" unless task
|
168
|
+
connect(task)
|
169
|
+
configuration.execute_task(task)
|
170
|
+
end
|
171
|
+
rescue Capistrano::NoMatchingServersError, Capistrano::NoSuchTaskError => error
|
172
|
+
warn "error: #{error.message}"
|
173
|
+
end
|
174
|
+
|
175
|
+
# Execute a command on the given list of servers.
|
176
|
+
def exec_command(command, servers)
|
177
|
+
command = command.gsub(/^(\s*)sudo\b|([|;&])\s*sudo\b/, "\\0 -p '#{configuration.sudo_prompt}'")
|
178
|
+
processor = configuration.sudo_behavior_callback(Configuration.default_io_proc)
|
179
|
+
sessions = servers.map { |server| configuration.sessions[server] }
|
180
|
+
options = configuration.add_default_command_options({})
|
181
|
+
cmd = Command.new(command, sessions, options.merge(:logger => configuration.logger), &processor)
|
182
|
+
previous = trap("INT") { cmd.stop! }
|
183
|
+
cmd.process!
|
184
|
+
rescue Capistrano::Error => error
|
185
|
+
warn "error: #{error.message}"
|
186
|
+
ensure
|
187
|
+
trap("INT", previous)
|
188
|
+
end
|
189
|
+
|
190
|
+
# Return the object that will be used to query input from the console.
|
191
|
+
# The returned object will quack (more or less) like Readline.
|
192
|
+
def reader
|
193
|
+
@reader ||= begin
|
194
|
+
require 'readline'
|
195
|
+
Readline
|
196
|
+
rescue LoadError
|
197
|
+
ReadlineFallback
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
# Prepare every little thing for the shell. Starts the background
|
202
|
+
# thread and generally gets things ready for the REPL.
|
203
|
+
def setup
|
204
|
+
configuration.logger.level = Capistrano::Logger::INFO
|
205
|
+
wait_for = 0.1
|
206
|
+
|
207
|
+
@mutex = Mutex.new
|
208
|
+
@bgthread = Thread.new do
|
209
|
+
loop do
|
210
|
+
ret = @mutex.synchronize { process_iteration(wait_for) }
|
211
|
+
sleep wait_for if !ret
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
# Set the given option to +value+.
|
217
|
+
def set_option(opt, value)
|
218
|
+
case opt
|
219
|
+
when "v" then
|
220
|
+
puts "setting log verbosity to #{value.to_i}"
|
221
|
+
configuration.logger.level = value.to_i
|
222
|
+
when "o" then
|
223
|
+
case value
|
224
|
+
when "vi" then
|
225
|
+
puts "using vi edit mode"
|
226
|
+
reader.vi_editing_mode
|
227
|
+
when "emacs" then
|
228
|
+
puts "using emacs edit mode"
|
229
|
+
reader.emacs_editing_mode
|
230
|
+
else
|
231
|
+
puts "unknown -o option #{value.inspect}"
|
232
|
+
end
|
233
|
+
else
|
234
|
+
puts "unknown setting #{opt.inspect}"
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
# Process a command. Interprets the scope_type (must be nil, "with", or
|
239
|
+
# "on") and the command. If no command is given, then the scope is made
|
240
|
+
# effective for all subsequent commands. If the scope value is "all",
|
241
|
+
# then the scope is unrestricted.
|
242
|
+
def process_command(scope_type, scope_value, command)
|
243
|
+
env_var = case scope_type
|
244
|
+
when "with" then "ROLES"
|
245
|
+
when "on" then "HOSTS"
|
246
|
+
end
|
247
|
+
|
248
|
+
old_var, ENV[env_var] = ENV[env_var], (scope_value == "all" ? nil : scope_value) if env_var
|
249
|
+
if command
|
250
|
+
begin
|
251
|
+
exec(command)
|
252
|
+
ensure
|
253
|
+
ENV[env_var] = old_var if env_var
|
254
|
+
end
|
255
|
+
else
|
256
|
+
puts "scoping #{scope_type} #{scope_value}"
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
# All open sessions, needed to satisfy the Command::Processable include
|
261
|
+
def sessions
|
262
|
+
configuration.sessions.values
|
263
|
+
end
|
264
|
+
end
|
265
|
+
end
|