capistrano 1.1.0 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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) }
|