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.
@@ -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
@@ -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
- required_version = [1,0,5]
9
- if !Version.check(required_version, ssh_version)
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
 
@@ -19,12 +19,12 @@ module Capistrano
19
19
  end
20
20
 
21
21
  MAJOR = 1
22
- MINOR = 1
22
+ MINOR = 2
23
23
  TINY = 0
24
24
 
25
25
  STRING = [MAJOR, MINOR, TINY].join(".")
26
26
 
27
- SSH_REQUIRED = [1,0,8]
27
+ SSH_REQUIRED = [1,0,10]
28
28
  SFTP_REQUIRED = [1,1,0]
29
29
  end
30
30
  end
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
@@ -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) }