capistrano 2.2.0 → 2.3.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.
Files changed (59) hide show
  1. data/CHANGELOG +29 -0
  2. data/README +4 -7
  3. data/bin/cap +0 -0
  4. data/bin/capify +0 -0
  5. data/lib/capistrano/command.rb +32 -38
  6. data/lib/capistrano/configuration/actions/file_transfer.rb +21 -13
  7. data/lib/capistrano/configuration/actions/invocation.rb +1 -1
  8. data/lib/capistrano/configuration/connections.rb +30 -20
  9. data/lib/capistrano/errors.rb +1 -1
  10. data/lib/capistrano/processable.rb +53 -0
  11. data/lib/capistrano/recipes/deploy/remote_dependency.rb +6 -0
  12. data/lib/capistrano/recipes/deploy/scm/git.rb +26 -11
  13. data/lib/capistrano/recipes/deploy/scm/none.rb +44 -0
  14. data/lib/capistrano/recipes/deploy/strategy/base.rb +6 -0
  15. data/lib/capistrano/recipes/deploy/strategy/copy.rb +74 -3
  16. data/lib/capistrano/recipes/deploy.rb +15 -13
  17. data/lib/capistrano/role.rb +0 -14
  18. data/lib/capistrano/server_definition.rb +5 -0
  19. data/lib/capistrano/shell.rb +21 -17
  20. data/lib/capistrano/ssh.rb +24 -58
  21. data/lib/capistrano/transfer.rb +216 -0
  22. data/lib/capistrano/version.rb +1 -1
  23. data/test/cli/execute_test.rb +1 -1
  24. data/test/cli/help_test.rb +1 -1
  25. data/test/cli/options_test.rb +1 -1
  26. data/test/cli/ui_test.rb +1 -1
  27. data/test/cli_test.rb +1 -1
  28. data/test/command_test.rb +31 -51
  29. data/test/configuration/actions/file_transfer_test.rb +21 -19
  30. data/test/configuration/actions/inspect_test.rb +1 -1
  31. data/test/configuration/actions/invocation_test.rb +6 -6
  32. data/test/configuration/callbacks_test.rb +1 -1
  33. data/test/configuration/connections_test.rb +11 -12
  34. data/test/configuration/execution_test.rb +1 -1
  35. data/test/configuration/loading_test.rb +1 -1
  36. data/test/configuration/namespace_dsl_test.rb +1 -1
  37. data/test/configuration/roles_test.rb +1 -1
  38. data/test/configuration/servers_test.rb +1 -1
  39. data/test/configuration/variables_test.rb +1 -1
  40. data/test/configuration_test.rb +1 -1
  41. data/test/deploy/scm/accurev_test.rb +1 -1
  42. data/test/deploy/scm/base_test.rb +1 -1
  43. data/test/deploy/scm/git_test.rb +10 -6
  44. data/test/deploy/scm/mercurial_test.rb +1 -1
  45. data/test/deploy/strategy/copy_test.rb +120 -27
  46. data/test/extensions_test.rb +1 -1
  47. data/test/logger_test.rb +1 -1
  48. data/test/server_definition_test.rb +1 -1
  49. data/test/shell_test.rb +27 -1
  50. data/test/ssh_test.rb +27 -21
  51. data/test/task_definition_test.rb +1 -1
  52. data/test/transfer_test.rb +160 -0
  53. data/test/utils.rb +30 -34
  54. data/test/version_test.rb +1 -1
  55. metadata +26 -14
  56. data/lib/capistrano/gateway.rb +0 -131
  57. data/lib/capistrano/upload.rb +0 -152
  58. data/test/gateway_test.rb +0 -167
  59. data/test/upload_test.rb +0 -131
@@ -1,152 +0,0 @@
1
- begin
2
- require 'rubygems'
3
- gem 'net-sftp', "< 1.99.0"
4
- rescue LoadError, NameError
5
- end
6
-
7
- require 'net/sftp'
8
- require 'net/sftp/operations/errors'
9
- require 'capistrano/errors'
10
-
11
- module Capistrano
12
- unless ENV['SKIP_VERSION_CHECK']
13
- require 'capistrano/version'
14
- require 'net/sftp/version'
15
- sftp_version = [Net::SFTP::Version::MAJOR, Net::SFTP::Version::MINOR, Net::SFTP::Version::TINY]
16
- required_version = [1,1,0]
17
- if !Capistrano::Version.check(required_version, sftp_version)
18
- raise "You have Net::SFTP #{sftp_version.join(".")}, but you need at least #{required_version.join(".")}. Net::SFTP will not be used."
19
- end
20
- end
21
-
22
- # This class encapsulates a single file upload to be performed in parallel
23
- # across multiple machines, using the SFTP protocol. Although it is intended
24
- # to be used primarily from within Capistrano, it may also be used standalone
25
- # if you need to simply upload a file to multiple servers.
26
- #
27
- # Basic Usage:
28
- #
29
- # begin
30
- # uploader = Capistrano::Upload.new(sessions, "remote-file.txt",
31
- # :data => "the contents of the file to upload")
32
- # uploader.process!
33
- # rescue Capistrano::UploadError => e
34
- # warn "Could not upload the file: #{e.message}"
35
- # end
36
- class Upload
37
- def self.process(sessions, filename, options)
38
- new(sessions, filename, options).process!
39
- end
40
-
41
- attr_reader :sessions, :filename, :options
42
- attr_reader :failed, :completed
43
-
44
- # Creates and prepares a new Upload instance. The +sessions+ parameter
45
- # must be an array of open Net::SSH sessions. The +filename+ is the name
46
- # (including path) of the destination file on the remote server. The
47
- # +options+ hash accepts the following keys (as symbols):
48
- #
49
- # * data: required. Should refer to a String containing the contents of
50
- # the file to upload.
51
- # * mode: optional. The "mode" of the destination file. Defaults to 0664.
52
- # * logger: optional. Should point to a Capistrano::Logger instance, if
53
- # given.
54
- def initialize(sessions, filename, options)
55
- raise ArgumentError, "you must specify the data to upload via the :data option" unless options[:data]
56
-
57
- @sessions = sessions
58
- @filename = filename
59
- @options = options
60
-
61
- @completed = @failed = 0
62
- @sftps = setup_sftp
63
- end
64
-
65
- # Uploads to all specified servers in parallel. If any one of the servers
66
- # fails, an exception will be raised (UploadError).
67
- def process!
68
- logger.debug "uploading #{filename}" if logger
69
- while running?
70
- @sftps.each do |sftp|
71
- next if sftp.channel[:done]
72
- begin
73
- sftp.channel.connection.process(true)
74
- rescue Net::SFTP::Operations::StatusException => error
75
- logger.important "uploading failed: #{error.description}", sftp.channel[:server] if logger
76
- failed!(sftp)
77
- end
78
- end
79
- sleep 0.01 # a brief respite, to keep the CPU from going crazy
80
- end
81
- logger.trace "upload finished" if logger
82
-
83
- if (failed = @sftps.select { |sftp| sftp.channel[:failed] }).any?
84
- hosts = failed.map { |sftp| sftp.channel[:server] }
85
- error = UploadError.new("upload of #{filename} failed on #{hosts.join(',')}")
86
- error.hosts = hosts
87
- raise error
88
- end
89
-
90
- self
91
- end
92
-
93
- private
94
-
95
- def logger
96
- options[:logger]
97
- end
98
-
99
- def setup_sftp
100
- sessions.map do |session|
101
- server = session.xserver
102
- sftp = session.sftp
103
- sftp.connect unless sftp.state == :open
104
-
105
- sftp.channel[:server] = server
106
- sftp.channel[:done] = false
107
- sftp.channel[:failed] = false
108
-
109
- real_filename = filename.gsub(/\$CAPISTRANO:HOST\$/, server.host)
110
- sftp.open(real_filename, IO::WRONLY | IO::CREAT | IO::TRUNC, options[:mode] || 0664) do |status, handle|
111
- break unless check_status(sftp, "open #{real_filename}", server, status)
112
-
113
- logger.info "uploading data to #{server}:#{real_filename}" if logger
114
- sftp.write(handle, options[:data] || "") do |status|
115
- break unless check_status(sftp, "write to #{server}:#{real_filename}", server, status)
116
- sftp.close_handle(handle) do
117
- logger.debug "done uploading data to #{server}:#{real_filename}" if logger
118
- completed!(sftp)
119
- end
120
- end
121
- end
122
-
123
- sftp
124
- end
125
- end
126
-
127
- def check_status(sftp, action, server, status)
128
- return true if status.code == Net::SFTP::Session::FX_OK
129
-
130
- logger.error "could not #{action} on #{server} (#{status.message})" if logger
131
- failed!(sftp)
132
-
133
- return false
134
- end
135
-
136
- def running?
137
- completed < @sftps.length
138
- end
139
-
140
- def failed!(sftp)
141
- completed!(sftp)
142
- @failed += 1
143
- sftp.channel[:failed] = true
144
- end
145
-
146
- def completed!(sftp)
147
- @completed += 1
148
- sftp.channel[:done] = true
149
- end
150
- end
151
-
152
- end
data/test/gateway_test.rb DELETED
@@ -1,167 +0,0 @@
1
- require "#{File.dirname(__FILE__)}/utils"
2
- require 'capistrano/gateway'
3
-
4
- class GatewayTest < Test::Unit::TestCase
5
- def teardown
6
- Thread.list { |t| t.kill unless Thread.current == t }
7
- end
8
-
9
- def test_initialize_should_open_and_set_session_value
10
- run_test_initialize_should_open_and_set_session_value
11
- end
12
-
13
- def test_initialize_when_connect_lags_should_open_and_set_session_value
14
- run_test_initialize_should_open_and_set_session_value do |expects|
15
- expects.with { |*args| sleep 0.2; true }
16
- end
17
- end
18
-
19
- def test_shutdown_without_any_open_connections_should_terminate_session
20
- gateway = new_gateway
21
- gateway.shutdown!
22
- assert !gateway.thread.alive?
23
- assert !gateway.session.looping?
24
- end
25
-
26
- def test_connect_to_should_start_local_ports_at_65535
27
- gateway = new_gateway
28
- expect_connect_to(:host => "127.0.0.1", :port => 65535).returns(result = sess_with_xserver("app1"))
29
- newsess = gateway.connect_to(server("app1"))
30
- assert_equal result, newsess
31
- assert_equal [65535, "app1", 22], gateway.session.forward.active_locals[65535]
32
- end
33
-
34
- def test_connect_to_should_decrement_port_and_retry_if_ports_are_in_use
35
- gateway = new_gateway(:reserved => lambda { |n| n > 65000 })
36
- expect_connect_to(:host => "127.0.0.1", :port => 65000).returns(result = sess_with_xserver("app1"))
37
- newsess = gateway.connect_to(server("app1"))
38
- assert_equal result, newsess
39
- assert_equal [65000, "app1", 22], gateway.session.forward.active_locals[65000]
40
- end
41
-
42
- def test_connect_to_should_honor_user_specification_in_server_definition
43
- gateway = new_gateway
44
- expect_connect_to(:host => "127.0.0.1", :user => "jamis", :port => 65535).returns(result = sess_with_xserver("app1"))
45
- newsess = gateway.connect_to(server("jamis@app1"))
46
- assert_equal result, newsess
47
- assert_equal [65535, "app1", 22], gateway.session.forward.active_locals[65535]
48
- end
49
-
50
- def test_connect_to_should_honor_port_specification_in_server_definition
51
- gateway = new_gateway
52
- expect_connect_to(:host => "127.0.0.1", :port => 65535).returns(result = sess_with_xserver("app1"))
53
- newsess = gateway.connect_to(server("app1:1234"))
54
- assert_equal result, newsess
55
- assert_equal [65535, "app1", 1234], gateway.session.forward.active_locals[65535]
56
- end
57
-
58
- def test_connect_to_should_set_xserver_to_tunnel_target
59
- gateway = new_gateway
60
- expect_connect_to(:host => "127.0.0.1", :port => 65535).returns(result = sess_with_xserver("app1"))
61
- newsess = gateway.connect_to(server("app1:1234"))
62
- assert_equal result, newsess
63
- end
64
-
65
- def test_shutdown_should_cancel_active_forwarded_ports
66
- gateway = new_gateway
67
- expect_connect_to(:host => "127.0.0.1", :port => 65535).returns(sess_with_xserver("app1"))
68
- gateway.connect_to(server("app1"))
69
- assert !gateway.session.forward.active_locals.empty?
70
- gateway.shutdown!
71
- assert gateway.session.forward.active_locals.empty?
72
- end
73
-
74
- def test_error_while_connecting_should_cause_connection_to_fail
75
- gateway = new_gateway
76
- expect_connect_to(:host => "127.0.0.1").raises(RuntimeError)
77
- gateway.expects(:warn).times(2)
78
- assert_raises(Capistrano::ConnectionError) { gateway.connect_to(server("app1")) }
79
- end
80
-
81
- def test_connection_error_should_include_accessor_with_host_array
82
- gateway = new_gateway
83
- expect_connect_to(:host => "127.0.0.1").raises(RuntimeError)
84
- gateway.expects(:warn).times(2)
85
-
86
- begin
87
- gateway.connect_to(server("app1"))
88
- flunk "expected an exception to be raised"
89
- rescue Capistrano::ConnectionError => e
90
- assert e.respond_to?(:hosts)
91
- assert_equal %w(app1), e.hosts.map { |h| h.to_s }
92
- end
93
- end
94
-
95
- private
96
-
97
- def sess_with_xserver(host)
98
- s = server(host)
99
- sess = mock("session")
100
- sess.expects(:xserver=).with { |v| v.host == host }
101
- sess
102
- end
103
-
104
- def expect_connect_to(options={})
105
- Capistrano::SSH.expects(:connect).with do |server,config|
106
- options.all? do |key, value|
107
- case key
108
- when :host then server.host == value
109
- when :user then server.user == value
110
- when :port then server.port == value
111
- else false
112
- end
113
- end
114
- end
115
- end
116
-
117
- def new_gateway(options={})
118
- expect_connect_to(:host => "capistrano").yields(MockSession.new(options))
119
- Capistrano::Gateway.new(server("capistrano"))
120
- end
121
-
122
- def run_test_initialize_should_open_and_set_session_value
123
- session = mock("Net::SSH session")
124
- session.expects(:loop)
125
- expectation = Capistrano::SSH.expects(:connect).yields(session)
126
- yield expectation if block_given?
127
- gateway = Capistrano::Gateway.new(server("capistrano"))
128
- gateway.thread.join
129
- assert_equal session, gateway.session
130
- end
131
-
132
- class MockForward
133
- attr_reader :active_locals
134
-
135
- def initialize(options)
136
- @options = options
137
- @active_locals = {}
138
- end
139
-
140
- def cancel_local(port)
141
- @active_locals.delete(port)
142
- end
143
-
144
- def local(lport, host, rport)
145
- raise Errno::EADDRINUSE if @options[:reserved] && @options[:reserved][lport]
146
- @active_locals[lport] = [lport, host, rport]
147
- end
148
- end
149
-
150
- class MockSession
151
- attr_reader :forward
152
-
153
- def initialize(options={})
154
- @forward = MockForward.new(options)
155
- end
156
-
157
- def looping?
158
- @looping
159
- end
160
-
161
- def loop
162
- @looping = true
163
- sleep 0.1 while yield
164
- @looping = false
165
- end
166
- end
167
- end
data/test/upload_test.rb DELETED
@@ -1,131 +0,0 @@
1
- require "#{File.dirname(__FILE__)}/utils"
2
- require 'capistrano/upload'
3
-
4
- class UploadTest < Test::Unit::TestCase
5
- def setup
6
- @mode = IO::WRONLY | IO::CREAT | IO::TRUNC
7
- end
8
-
9
- def test_initialize_should_raise_error_if_data_is_missing
10
- assert_raises(ArgumentError) do
11
- Capistrano::Upload.new([], "test.txt", :foo => "bar")
12
- end
13
- end
14
-
15
- def test_initialize_should_get_sftp_for_each_session
16
- new_sftp = Proc.new do |state|
17
- sftp = mock("sftp", :state => state, :open => nil)
18
- sftp.expects(:connect) unless state == :open
19
- sftp.stubs(:channel).returns({})
20
- sftp
21
- end
22
-
23
- sessions = [mock("session", :xserver => server("a"), :sftp => new_sftp[:closed]),
24
- mock("session", :xserver => server("b"), :sftp => new_sftp[:closed]),
25
- mock("session", :xserver => server("c"), :sftp => new_sftp[:open])]
26
- Capistrano::Upload.new(sessions, "test.txt", :data => "data")
27
- end
28
-
29
- def test_self_process_should_instantiate_uploader_and_start_process
30
- Capistrano::Upload.expects(:new).with([:s1, :s2], "test.txt", :data => "data").returns(mock(:process! => nil))
31
- Capistrano::Upload.process([:s1, :s2], "test.txt", :data => "data")
32
- end
33
-
34
- def test_process_when_sftp_open_fails_should_raise_error
35
- sftp = mock_sftp
36
- sftp.expects(:open).with("test.txt", @mode, 0664).yields(mock("status", :code => "bad status", :message => "bad status"), :file_handle)
37
- session = mock("session", :sftp => sftp, :xserver => server("capistrano"))
38
- upload = Capistrano::Upload.new([session], "test.txt", :data => "data", :logger => stub_everything)
39
- assert_raises(Capistrano::UploadError) { upload.process! }
40
- assert_equal 1, upload.failed
41
- assert_equal 1, upload.completed
42
- end
43
-
44
- def test_process_when_sftp_write_fails_should_raise_error
45
- sftp = mock_sftp
46
- sftp.expects(:open).with("test.txt", @mode, 0664).yields(mock("status1", :code => Net::SFTP::Session::FX_OK), :file_handle)
47
- sftp.expects(:write).with(:file_handle, "data").yields(mock("status2", :code => "bad status", :message => "bad status"))
48
- session = mock("session", :sftp => sftp, :xserver => server("capistrano"))
49
- upload = Capistrano::Upload.new([session], "test.txt", :data => "data", :logger => stub_everything)
50
- assert_raises(Capistrano::UploadError) { upload.process! }
51
- assert_equal 1, upload.failed
52
- assert_equal 1, upload.completed
53
- end
54
-
55
- def test_upload_error_should_include_accessor_with_host_array
56
- sftp = mock_sftp
57
- sftp.expects(:open).with("test.txt", @mode, 0664).yields(mock("status1", :code => Net::SFTP::Session::FX_OK), :file_handle)
58
- sftp.expects(:write).with(:file_handle, "data").yields(mock("status2", :code => "bad status", :message => "bad status"))
59
- session = mock("session", :sftp => sftp, :xserver => server("capistrano"))
60
- upload = Capistrano::Upload.new([session], "test.txt", :data => "data", :logger => stub_everything)
61
-
62
- begin
63
- upload.process!
64
- flunk "expected an exception to be raised"
65
- rescue Capistrano::UploadError => e
66
- assert e.respond_to?(:hosts)
67
- assert_equal %w(capistrano), e.hosts.map { |h| h.to_s }
68
- end
69
- end
70
-
71
- def test_process_when_sftp_succeeds_should_raise_nothing
72
- sftp = mock_sftp
73
- sftp.expects(:open).with("test.txt", @mode, 0664).yields(mock("status1", :code => Net::SFTP::Session::FX_OK), :file_handle)
74
- sftp.expects(:write).with(:file_handle, "data").yields(mock("status2", :code => Net::SFTP::Session::FX_OK))
75
- sftp.expects(:close_handle).with(:file_handle).yields
76
- session = mock("session", :sftp => sftp, :xserver => server("capistrano"))
77
- upload = Capistrano::Upload.new([session], "test.txt", :data => "data", :logger => stub_everything)
78
- assert_nothing_raised { upload.process! }
79
- assert_equal 0, upload.failed
80
- assert_equal 1, upload.completed
81
- end
82
-
83
- def test_process_should_loop_while_running
84
- con = mock("connection")
85
- con.expects(:process).with(true).times(10)
86
- channel = {}
87
- channel.expects(:connection).returns(con).times(10)
88
- sftp = mock("sftp", :state => :open, :open => nil)
89
- sftp.stubs(:channel).returns(channel)
90
- session = mock("session", :sftp => sftp, :xserver => server("capistrano"))
91
- upload = Capistrano::Upload.new([session], "test.txt", :data => "data")
92
- upload.expects(:running?).times(11).returns(*([true]*10 + [false]))
93
- upload.process!
94
- end
95
-
96
- def test_process_should_loop_but_not_process_done_channels
97
- new_sftp = Proc.new do |done|
98
- channel = {}
99
- channel[:needs_done] = done
100
-
101
- if !done
102
- con = mock("connection")
103
- con.expects(:process).with(true).times(10)
104
- channel.expects(:connection).returns(con).times(10)
105
- end
106
-
107
- sftp = mock("sftp", :state => :open, :open => nil)
108
- sftp.stubs(:channel).returns(channel)
109
- sftp
110
- end
111
-
112
- sessions = [stub("session", :sftp => new_sftp[true], :xserver => server("capistrano")),
113
- stub("session", :sftp => new_sftp[false], :xserver => server("cap2"))]
114
- upload = Capistrano::Upload.new(sessions, "test.txt", :data => "data")
115
-
116
- # make sure the sftp channels we wanted to be done, start as done
117
- # (Upload.new marks each channel as not-done, so we have to do it here)
118
- sessions.each { |s| s.sftp.channel[:done] = true if s.sftp.channel[:needs_done] }
119
- upload.expects(:running?).times(11).returns(*([true]*10 + [false]))
120
- upload.process!
121
- end
122
-
123
- private
124
-
125
- def mock_sftp
126
- sftp = mock("sftp", :state => :open)
127
- sftp.stubs(:channel).returns(Hash.new)
128
- yield sftp if block_given?
129
- sftp
130
- end
131
- end