capistrano 1.1.0 → 1.2.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.
- data/CHANGELOG +241 -0
- data/MIT-LICENSE +20 -0
- data/README +35 -0
- data/THANKS +4 -0
- data/lib/capistrano/actor.rb +111 -24
- data/lib/capistrano/cli.rb +18 -9
- data/lib/capistrano/command.rb +8 -0
- data/lib/capistrano/configuration.rb +1 -1
- data/lib/capistrano/gateway.rb +29 -43
- data/lib/capistrano/recipes/standard.rb +38 -10
- data/lib/capistrano/scm/base.rb +1 -2
- data/lib/capistrano/scm/cvs.rb +5 -0
- data/lib/capistrano/scm/mercurial.rb +83 -0
- data/lib/capistrano/scm/subversion.rb +18 -18
- data/lib/capistrano/shell.rb +224 -0
- data/lib/capistrano/ssh.rb +2 -3
- data/lib/capistrano/version.rb +2 -2
- data/test/actor_test.rb +104 -5
- data/test/scm/cvs_test.rb +10 -0
- data/test/scm/subversion_test.rb +6 -6
- metadata +95 -79
@@ -0,0 +1,224 @@
|
|
1
|
+
require 'thread'
|
2
|
+
|
3
|
+
# The Capistrano::Shell class is the guts of the "shell" task. It implements
|
4
|
+
# an interactive REPL interface that users can employ to execute tasks and
|
5
|
+
# commands. It makes for a GREAT way to monitor systems, and perform quick
|
6
|
+
# maintenance on one or more machines.
|
7
|
+
|
8
|
+
module Capistrano
|
9
|
+
class Shell
|
10
|
+
# The actor instance employed by this shell
|
11
|
+
attr_reader :actor
|
12
|
+
|
13
|
+
# Instantiate a new shell and begin executing it immediately.
|
14
|
+
def self.run!(actor)
|
15
|
+
new(actor).run!
|
16
|
+
end
|
17
|
+
|
18
|
+
# Instantiate a new shell
|
19
|
+
def initialize(actor)
|
20
|
+
@actor = actor
|
21
|
+
end
|
22
|
+
|
23
|
+
# Start the shell running. This method will block until the shell
|
24
|
+
# terminates.
|
25
|
+
def run!
|
26
|
+
setup
|
27
|
+
|
28
|
+
puts <<-INTRO
|
29
|
+
====================================================================
|
30
|
+
Welcome to the interactive Capistrano shell! This is an experimental
|
31
|
+
feature, and is liable to change in future releases.
|
32
|
+
--------------------------------------------------------------------
|
33
|
+
INTRO
|
34
|
+
|
35
|
+
loop do
|
36
|
+
command = @reader.readline("cap> ", true)
|
37
|
+
|
38
|
+
case command ? command.strip : command
|
39
|
+
when "" then next
|
40
|
+
when "help" then help
|
41
|
+
when nil, "quit", "exit" then
|
42
|
+
puts if command.nil?
|
43
|
+
puts "exiting"
|
44
|
+
break
|
45
|
+
when /^set -(\w)\s*(\S+)/
|
46
|
+
set_option($1, $2)
|
47
|
+
when /^(?:(with|on)\s*(\S+))?\s*(\S.*)?/i
|
48
|
+
process_command($1, $2, $3)
|
49
|
+
else
|
50
|
+
raise "eh?"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
@bgthread.kill
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
# A Readline replacement for platforms where readline is either
|
60
|
+
# unavailable, or has not been installed.
|
61
|
+
class ReadlineFallback
|
62
|
+
def self.readline(prompt, *args)
|
63
|
+
STDOUT.print(prompt)
|
64
|
+
STDOUT.flush
|
65
|
+
STDIN.gets
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# Display a verbose help message.
|
70
|
+
def help
|
71
|
+
puts <<-HELP
|
72
|
+
Welcome to the interactive Capistrano shell! To quit, just type quit,
|
73
|
+
or exit. Or press ctrl-D. This shell is still experimental, so expect
|
74
|
+
it to change (or even disappear!) in future releases.
|
75
|
+
|
76
|
+
To execute a command on all servers, just type it directly, like:
|
77
|
+
|
78
|
+
cap> echo ping
|
79
|
+
|
80
|
+
To execute a command on a specific set of servers, specify an 'on' clause.
|
81
|
+
Note that if you specify more than one host name, they must be comma-
|
82
|
+
delimited, with NO SPACES between them.
|
83
|
+
|
84
|
+
cap> on app1.foo.com,app2.foo.com echo ping
|
85
|
+
|
86
|
+
To execute a command on all servers matching a set of roles:
|
87
|
+
|
88
|
+
cap> with app,db echo ping
|
89
|
+
|
90
|
+
To execute a Capistrano task, prefix the name with a bang:
|
91
|
+
|
92
|
+
cap> !deploy
|
93
|
+
|
94
|
+
You can specify multiple tasks to execute, separated by spaces:
|
95
|
+
|
96
|
+
cap> !update_code symlink
|
97
|
+
|
98
|
+
And, lastly, you can specify 'on' or 'with' with tasks:
|
99
|
+
|
100
|
+
cap> on app6.foo.com !setup
|
101
|
+
|
102
|
+
Enjoy!
|
103
|
+
HELP
|
104
|
+
end
|
105
|
+
|
106
|
+
# Determine which servers the given task requires a connection to, and
|
107
|
+
# establish connections to them if necessary. Return the list of
|
108
|
+
# servers (names).
|
109
|
+
def connect(task)
|
110
|
+
servers = task.servers(:refresh)
|
111
|
+
needing_connections = servers - actor.sessions.keys
|
112
|
+
unless needing_connections.empty?
|
113
|
+
puts "[establishing connection(s) to #{needing_connections.join(', ')}]"
|
114
|
+
actor.send(:establish_connections, servers)
|
115
|
+
end
|
116
|
+
servers
|
117
|
+
end
|
118
|
+
|
119
|
+
# Execute the given command. If the command is prefixed by an exclamation
|
120
|
+
# mark, it is assumed to refer to another capistrano task, which will
|
121
|
+
# be invoked. Otherwise, it is executed as a command on all associated
|
122
|
+
# servers.
|
123
|
+
def exec(command)
|
124
|
+
if command[0] == ?!
|
125
|
+
exec_tasks(command[1..-1].split)
|
126
|
+
else
|
127
|
+
servers = connect(actor.current_task)
|
128
|
+
exec_command(command, servers)
|
129
|
+
end
|
130
|
+
ensure
|
131
|
+
STDOUT.flush
|
132
|
+
end
|
133
|
+
|
134
|
+
# Given an array of task names, invoke them in sequence.
|
135
|
+
def exec_tasks(list)
|
136
|
+
list.each do |task_name|
|
137
|
+
task = task_name.to_sym
|
138
|
+
connect(actor.tasks[task])
|
139
|
+
actor.send(task)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
# Execute a command on the given list of servers.
|
144
|
+
def exec_command(command, servers)
|
145
|
+
processor = Proc.new do |ch, stream, out|
|
146
|
+
# TODO: more robust prompt detection
|
147
|
+
out.each do |line|
|
148
|
+
if stream == :out
|
149
|
+
if out =~ /Password:\s*/i
|
150
|
+
ch.send_data "#{actor.password}\n"
|
151
|
+
else
|
152
|
+
puts "[#{ch[:host]}] #{line.chomp}"
|
153
|
+
end
|
154
|
+
elsif stream == :err
|
155
|
+
puts "[#{ch[:host]} ERR] #{line.chomp}"
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
cmd = Command.new(servers, command, processor, {}, actor)
|
161
|
+
previous = trap("INT") { cmd.stop! }
|
162
|
+
cmd.process! rescue nil
|
163
|
+
trap("INT", previous)
|
164
|
+
end
|
165
|
+
|
166
|
+
# Prepare every little thing for the shell. Starts the background
|
167
|
+
# thread and generally gets things ready for the REPL.
|
168
|
+
def setup
|
169
|
+
begin
|
170
|
+
require 'readline'
|
171
|
+
@reader = Readline
|
172
|
+
rescue LoadError
|
173
|
+
@reader = ReadlineFallback
|
174
|
+
end
|
175
|
+
|
176
|
+
@mutex = Mutex.new
|
177
|
+
@bgthread = Thread.new do
|
178
|
+
loop do
|
179
|
+
ready = actor.sessions.values.select { |sess| sess.connection.reader_ready? }
|
180
|
+
if ready.empty?
|
181
|
+
sleep 0.1
|
182
|
+
else
|
183
|
+
@mutex.synchronize do
|
184
|
+
ready.each { |session| session.connection.process(true) }
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
# Set the given option to +value+.
|
192
|
+
def set_option(opt, value)
|
193
|
+
case opt
|
194
|
+
when "v" then
|
195
|
+
puts "setting log verbosity to #{value.to_i}"
|
196
|
+
actor.logger.level = value.to_i
|
197
|
+
else
|
198
|
+
puts "unknown setting #{value.inspect}"
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
# Process a command. Interprets the scope_type (must be nil, "with", or
|
203
|
+
# "on") and the command. If no command is given, then the scope is made
|
204
|
+
# effective for all subsequent commands. If the scope value is "all",
|
205
|
+
# then the scope is unrestricted.
|
206
|
+
def process_command(scope_type, scope_value, command)
|
207
|
+
env_var = case scope_type
|
208
|
+
when "with" then "ROLES"
|
209
|
+
when "on" then "HOSTS"
|
210
|
+
end
|
211
|
+
|
212
|
+
old_var, ENV[env_var] = ENV[env_var], (scope_value == "all" ? nil : scope_value) if env_var
|
213
|
+
if command
|
214
|
+
begin
|
215
|
+
@mutex.synchronize { exec(command) }
|
216
|
+
ensure
|
217
|
+
ENV[env_var] = old_var if env_var
|
218
|
+
end
|
219
|
+
else
|
220
|
+
puts "scoping #{scope_type} #{scope_value}"
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
data/lib/capistrano/ssh.rb
CHANGED
@@ -5,9 +5,8 @@ module Capistrano
|
|
5
5
|
require 'capistrano/version'
|
6
6
|
require 'net/ssh/version'
|
7
7
|
ssh_version = [Net::SSH::Version::MAJOR, Net::SSH::Version::MINOR, Net::SSH::Version::TINY]
|
8
|
-
|
9
|
-
|
10
|
-
raise "You have Net::SSH #{ssh_version.join(".")}, but you need at least #{required_version.join(".")}"
|
8
|
+
if !Version.check(Version::SSH_REQUIRED, ssh_version)
|
9
|
+
raise "You have Net::SSH #{ssh_version.join(".")}, but you need at least #{Version::SSH_REQUIRED.join(".")}"
|
11
10
|
end
|
12
11
|
end
|
13
12
|
|
data/lib/capistrano/version.rb
CHANGED
data/test/actor_test.rb
CHANGED
@@ -55,11 +55,16 @@ class ActorTest < Test::Unit::TestCase
|
|
55
55
|
end
|
56
56
|
end
|
57
57
|
|
58
|
-
class MockConfiguration
|
58
|
+
class MockConfiguration < Capistrano::Configuration
|
59
59
|
Role = Struct.new(:host, :options)
|
60
60
|
|
61
61
|
attr_accessor :gateway, :pretend
|
62
62
|
|
63
|
+
def initialize(*args)
|
64
|
+
super
|
65
|
+
@logger = Capistrano::Logger.new(:output => StringIO.new)
|
66
|
+
end
|
67
|
+
|
63
68
|
def delegated_method
|
64
69
|
"result of method"
|
65
70
|
end
|
@@ -78,10 +83,6 @@ class ActorTest < Test::Unit::TestCase
|
|
78
83
|
def roles
|
79
84
|
ROLES
|
80
85
|
end
|
81
|
-
|
82
|
-
def logger
|
83
|
-
@logger ||= Capistrano::Logger.new(:output => StringIO.new)
|
84
|
-
end
|
85
86
|
end
|
86
87
|
|
87
88
|
module CustomExtension
|
@@ -93,8 +94,19 @@ class ActorTest < Test::Unit::TestCase
|
|
93
94
|
def setup
|
94
95
|
TestingCommand.reset!
|
95
96
|
@actor = TestActor.new(MockConfiguration.new)
|
97
|
+
ENV["ROLES"] = nil
|
98
|
+
ENV["HOSTS"] = nil
|
96
99
|
end
|
97
100
|
|
101
|
+
def test_previous_release_returns_nil_with_one_release
|
102
|
+
class << @actor
|
103
|
+
def releases
|
104
|
+
["1234567890"]
|
105
|
+
end
|
106
|
+
end
|
107
|
+
assert_equal @actor.previous_release, nil
|
108
|
+
end
|
109
|
+
|
98
110
|
def test_define_task_creates_method
|
99
111
|
@actor.define_task :hello do
|
100
112
|
"result"
|
@@ -157,6 +169,26 @@ class ActorTest < Test::Unit::TestCase
|
|
157
169
|
assert_equal [:goodbye, :hello], @actor.history
|
158
170
|
end
|
159
171
|
|
172
|
+
def test_rollback_uses_roles_for_associated_task
|
173
|
+
@actor.define_task :inner, :roles => :db do
|
174
|
+
on_rollback { run "error" }
|
175
|
+
run "go"
|
176
|
+
raise "fail"
|
177
|
+
end
|
178
|
+
|
179
|
+
@actor.define_task :outer do
|
180
|
+
transaction do
|
181
|
+
inner
|
182
|
+
end
|
183
|
+
run "done"
|
184
|
+
end
|
185
|
+
|
186
|
+
assert_raise(RuntimeError) { @actor.outer }
|
187
|
+
|
188
|
+
assert TestingCommand.invoked?
|
189
|
+
assert_equal %w(01.example.com 02.example.com all.example.com), @actor.sessions.keys.sort
|
190
|
+
end
|
191
|
+
|
160
192
|
def test_delegates_to_configuration
|
161
193
|
@actor.define_task :hello do
|
162
194
|
delegated_method
|
@@ -190,6 +222,16 @@ class ActorTest < Test::Unit::TestCase
|
|
190
222
|
assert_equal %w(01.example.com 02.example.com all.example.com), @actor.sessions.keys.sort
|
191
223
|
end
|
192
224
|
|
225
|
+
def test_run_in_task_with_single_role_selects_that_role_from_environment
|
226
|
+
ENV["ROLES"] = "app"
|
227
|
+
@actor.define_task :foo, :roles => :db do
|
228
|
+
run "do this"
|
229
|
+
end
|
230
|
+
|
231
|
+
@actor.foo
|
232
|
+
assert_equal %w(05.example.com 06.example.com 07.example.com all.example.com), @actor.sessions.keys.sort
|
233
|
+
end
|
234
|
+
|
193
235
|
def test_run_in_task_with_multiple_roles_selects_those_roles
|
194
236
|
@actor.define_task :foo, :roles => [:db, :web] do
|
195
237
|
run "do this"
|
@@ -199,6 +241,16 @@ class ActorTest < Test::Unit::TestCase
|
|
199
241
|
assert_equal %w(01.example.com 02.example.com 03.example.com 04.example.com all.example.com), @actor.sessions.keys.sort
|
200
242
|
end
|
201
243
|
|
244
|
+
def test_run_in_task_with_multiple_roles_selects_those_roles_from_environment
|
245
|
+
ENV["ROLES"] = "app,db"
|
246
|
+
@actor.define_task :foo, :roles => [:db, :web] do
|
247
|
+
run "do this"
|
248
|
+
end
|
249
|
+
|
250
|
+
@actor.foo
|
251
|
+
assert_equal %w(01.example.com 02.example.com 05.example.com 06.example.com 07.example.com all.example.com), @actor.sessions.keys.sort
|
252
|
+
end
|
253
|
+
|
202
254
|
def test_run_in_task_with_only_restricts_selected_roles
|
203
255
|
@actor.define_task :foo, :roles => :db, :only => { :primary => true } do
|
204
256
|
run "do this"
|
@@ -208,6 +260,53 @@ class ActorTest < Test::Unit::TestCase
|
|
208
260
|
assert_equal %w(01.example.com), @actor.sessions.keys.sort
|
209
261
|
end
|
210
262
|
|
263
|
+
def test_run_in_task_with_except_restricts_selected_roles
|
264
|
+
@actor.define_task :foo, :roles => :db, :except => { :primary => true } do
|
265
|
+
run "do this"
|
266
|
+
end
|
267
|
+
|
268
|
+
@actor.foo
|
269
|
+
assert_equal %w(02.example.com all.example.com), @actor.sessions.keys.sort
|
270
|
+
end
|
271
|
+
|
272
|
+
def test_run_in_task_with_single_host_selected
|
273
|
+
@actor.define_task :foo, :hosts => "01.example.com" do
|
274
|
+
run "do this"
|
275
|
+
end
|
276
|
+
|
277
|
+
@actor.foo
|
278
|
+
assert_equal %w(01.example.com), @actor.sessions.keys.sort
|
279
|
+
end
|
280
|
+
|
281
|
+
def test_run_in_task_with_single_host_selected_from_environment
|
282
|
+
ENV["HOSTS"] = "02.example.com"
|
283
|
+
@actor.define_task :foo, :hosts => "01.example.com" do
|
284
|
+
run "do this"
|
285
|
+
end
|
286
|
+
|
287
|
+
@actor.foo
|
288
|
+
assert_equal %w(02.example.com), @actor.sessions.keys.sort
|
289
|
+
end
|
290
|
+
|
291
|
+
def test_run_in_task_with_multiple_hosts_selected
|
292
|
+
@actor.define_task :foo, :hosts => [ "01.example.com", "07.example.com" ] do
|
293
|
+
run "do this"
|
294
|
+
end
|
295
|
+
|
296
|
+
@actor.foo
|
297
|
+
assert_equal %w(01.example.com 07.example.com), @actor.sessions.keys.sort
|
298
|
+
end
|
299
|
+
|
300
|
+
def test_run_in_task_with_multiple_hosts_selected_from_environment
|
301
|
+
ENV["HOSTS"] = "02.example.com,06.example.com"
|
302
|
+
@actor.define_task :foo, :hosts => [ "01.example.com", "07.example.com" ] do
|
303
|
+
run "do this"
|
304
|
+
end
|
305
|
+
|
306
|
+
@actor.foo
|
307
|
+
assert_equal %w(02.example.com 06.example.com), @actor.sessions.keys.sort
|
308
|
+
end
|
309
|
+
|
211
310
|
def test_establish_connection_uses_gateway_if_specified
|
212
311
|
@actor.configuration.gateway = "10.example.com"
|
213
312
|
@actor.define_task :foo, :roles => :db do
|
data/test/scm/cvs_test.rb
CHANGED
@@ -183,4 +183,14 @@ MSG
|
|
183
183
|
@scm = CvsTest.new(@config)
|
184
184
|
assert_equal "default-branch", @scm.current_branch
|
185
185
|
end
|
186
|
+
|
187
|
+
def test_default_local
|
188
|
+
@config = MockConfiguration.new
|
189
|
+
@config[:repository] = ":ext:joetester@rubyforge.org:/hello/world"
|
190
|
+
@config[:cvs] = "/path/to/cvs"
|
191
|
+
@config[:password] = "chocolatebrownies"
|
192
|
+
@config[:now] = Time.utc(2005,8,24,12,0,0)
|
193
|
+
@scm = CvsTest.new(@config)
|
194
|
+
assert_equal ".", @scm.configuration.local
|
195
|
+
end
|
186
196
|
end
|
data/test/scm/subversion_test.rb
CHANGED
@@ -74,12 +74,6 @@ MSG
|
|
74
74
|
assert_equal "/hello/world", @scm.last_path
|
75
75
|
end
|
76
76
|
|
77
|
-
def test_latest_revision_searching_upwards
|
78
|
-
@scm.story = [ "-----------------------------\n", @log_msg ]
|
79
|
-
assert_equal "1967", @scm.latest_revision
|
80
|
-
assert_equal "/hello", @scm.last_path
|
81
|
-
end
|
82
|
-
|
83
77
|
def test_checkout
|
84
78
|
@actor.story = []
|
85
79
|
assert_nothing_raised { @scm.checkout(@actor) }
|
@@ -114,6 +108,12 @@ MSG
|
|
114
108
|
assert_equal ["chocolatebrownies\n"], @actor.channels.last.sent_data
|
115
109
|
end
|
116
110
|
|
111
|
+
def test_checkout_needs_https_certificate
|
112
|
+
@actor.story = [[:out, "(R)eject, accept (t)emporarily or accept (p)ermanently? "]]
|
113
|
+
assert_nothing_raised { @scm.checkout(@actor) }
|
114
|
+
assert_equal ["t\n"], @actor.channels.last.sent_data
|
115
|
+
end
|
116
|
+
|
117
117
|
def test_checkout_needs_alternative_ssh_password
|
118
118
|
@actor.story = [[:out, "someone's password: "]]
|
119
119
|
assert_nothing_raised { @scm.checkout(@actor) }
|