capistrano 1.1.0 → 1.2.0

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