capistrano 1.1.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 (37) hide show
  1. data/bin/cap +11 -0
  2. data/examples/sample.rb +113 -0
  3. data/lib/capistrano.rb +1 -0
  4. data/lib/capistrano/actor.rb +438 -0
  5. data/lib/capistrano/cli.rb +295 -0
  6. data/lib/capistrano/command.rb +90 -0
  7. data/lib/capistrano/configuration.rb +243 -0
  8. data/lib/capistrano/extensions.rb +38 -0
  9. data/lib/capistrano/gateway.rb +118 -0
  10. data/lib/capistrano/generators/rails/deployment/deployment_generator.rb +25 -0
  11. data/lib/capistrano/generators/rails/deployment/templates/capistrano.rake +46 -0
  12. data/lib/capistrano/generators/rails/deployment/templates/deploy.rb +122 -0
  13. data/lib/capistrano/generators/rails/loader.rb +20 -0
  14. data/lib/capistrano/logger.rb +59 -0
  15. data/lib/capistrano/recipes/standard.rb +242 -0
  16. data/lib/capistrano/recipes/templates/maintenance.rhtml +53 -0
  17. data/lib/capistrano/scm/base.rb +62 -0
  18. data/lib/capistrano/scm/baz.rb +118 -0
  19. data/lib/capistrano/scm/bzr.rb +70 -0
  20. data/lib/capistrano/scm/cvs.rb +124 -0
  21. data/lib/capistrano/scm/darcs.rb +27 -0
  22. data/lib/capistrano/scm/perforce.rb +139 -0
  23. data/lib/capistrano/scm/subversion.rb +122 -0
  24. data/lib/capistrano/ssh.rb +39 -0
  25. data/lib/capistrano/transfer.rb +90 -0
  26. data/lib/capistrano/utils.rb +26 -0
  27. data/lib/capistrano/version.rb +30 -0
  28. data/test/actor_test.rb +294 -0
  29. data/test/command_test.rb +43 -0
  30. data/test/configuration_test.rb +233 -0
  31. data/test/fixtures/config.rb +5 -0
  32. data/test/fixtures/custom.rb +3 -0
  33. data/test/scm/cvs_test.rb +186 -0
  34. data/test/scm/subversion_test.rb +137 -0
  35. data/test/ssh_test.rb +104 -0
  36. data/test/utils.rb +50 -0
  37. metadata +107 -0
@@ -0,0 +1,139 @@
1
+ require 'capistrano/scm/base'
2
+
3
+ module Capistrano
4
+ module SCM
5
+
6
+ # An SCM module for using perforce as your source control tool.
7
+ # This module can explicitly selected by placing the following line
8
+ # in your configuration:
9
+ #
10
+ # set :scm, :perforce
11
+ #
12
+ # Also, this module accepts a <tt>:p4</tt> configuration variable,
13
+ # which (if specified) will be used as the full path to the p4
14
+ # executable on the remote machine:
15
+ #
16
+ # set :p4, "/usr/local/bin/p4"
17
+ #
18
+ # This module accepts another <tt>:p4sync_flags</tt> configuration
19
+ # variable, which (if specified) can add extra options. This setting
20
+ # defaults to the value "-f" which forces resynchronization.
21
+ #
22
+ # set :p4sync_flags, "-f"
23
+ #
24
+ # This module accepts another <tt>:p4client_root</tt> configuration
25
+ # variable to handle mapping adjustments. Perforce doesn't have the
26
+ # ability to sync to a specific directory (e.g. the release_path); the
27
+ # location being synced to is defined by the client-spec. As such, we
28
+ # sync the client-spec (defined by <tt>p4client</tt> and then copy from
29
+ # the determined root of the client-spec mappings to the release_path.
30
+ # In the unlikely event that your client-spec mappings introduces
31
+ # directory structure above the rails structure, you can override the
32
+ # may need to specify the directory
33
+ #
34
+ # set :p4client_root, "/user/rmcmahon/project1/code"
35
+ #
36
+ # Finally, the module accepts a <tt>p4diff2_options</tt> configuration
37
+ # variable. This can be used to manipulate the output when running a
38
+ # diff between what is deployed, and another revision. This option
39
+ # defaults to "-u". Run 'p4 help diff2' for other options.
40
+ #
41
+ class Perforce < Base
42
+
43
+ def latest_revision
44
+ configuration.logger.debug "querying latest revision..." unless @latest_revision
45
+ @latest_revision = `#{local_p4} counter change`.strip
46
+ @latest_revision
47
+ end
48
+
49
+ # Return the number of the revision currently deployed.
50
+ def current_revision(actor)
51
+ latest = actor.releases.last
52
+ grep = %(grep " #{latest}$" #{configuration.deploy_to}/revisions.log)
53
+ result = ""
54
+ actor.run(grep, :once => true) do |ch, str, out|
55
+ result << out if str == :out
56
+ raise "could not determine current revision" if str == :err
57
+ end
58
+
59
+ date, time, user, rev, dir = result.split
60
+ raise "current revision not found in revisions.log" unless dir == latest
61
+ rev.to_i
62
+ end
63
+
64
+ # Return a string containing the diff between the two revisions. +from+
65
+ # and +to+ may be in any format that p4 recognizes as a valid revision
66
+ # identifiers. If +from+ is +nil+, it defaults to the last deployed
67
+ # revision. If +to+ is +nil+, it defaults to #head.
68
+ def diff(actor, from=nil, to=nil)
69
+ from ||= "@#{current_revision(actor)}"
70
+ to ||= "#head"
71
+ p4client = configuration[:p4client]
72
+ p4diff2_options = configuration[:p4diff2_options]||"-u -db"
73
+ `#{local_p4} diff2 #{p4diff2_options} //#{p4client}/...#{from} //#{p4client}/...#{to}`
74
+ end
75
+
76
+ # Syncronizes (on all servers associated with the current task) the head
77
+ # revision of the code. Uses the given actor instance to execute the command.
78
+ #
79
+ def checkout(actor)
80
+ p4sync_flags = configuration[:p4sync_flags] || "-f"
81
+ p4client_root = configuration[:p4client_root] || "`#{remote_p4} client -o | grep ^Root | cut -f2`"
82
+ command = "#{remote_p4} sync #{p4sync_flags} && cp -rf #{p4client_root} #{actor.release_path};"
83
+ run_checkout(actor, command, &p4_stream_handler(actor))
84
+ end
85
+
86
+ def update(actor)
87
+ raise "#{self.class} doesn't support update(actor)"
88
+ end
89
+
90
+ private
91
+
92
+ def local_p4
93
+ add_standard_p4_options('p4')
94
+ end
95
+
96
+ def remote_p4
97
+ p4_cmd = configuration[:p4] || 'p4'
98
+ add_standard_p4_options(p4_cmd)
99
+ end
100
+
101
+ def add_standard_p4_options(p4_location)
102
+ check_settings
103
+ p4_cmd = p4_location
104
+ p4_cmd = "#{p4_cmd} -p #{configuration[:p4port]}" if configuration[:p4port]
105
+ p4_cmd = "#{p4_cmd} -u #{configuration[:p4user]}" if configuration[:p4user]
106
+ p4_cmd = "#{p4_cmd} -P #{configuration[:p4passwd]}" if configuration[:p4passwd]
107
+ p4_cmd = "#{p4_cmd} -c #{configuration[:p4client]}" if configuration[:p4client]
108
+ p4_cmd
109
+ end
110
+
111
+ def check_settings
112
+ check_setting(:p4port, "Add set :p4port, <your perforce server details e.g. my.p4.server:1666> to deploy.rb")
113
+ check_setting(:p4user, "Add set :p4user, <your production build username> to deploy.rb")
114
+ check_setting(:p4passwd, "Add set :p4passwd, <your build user password> to deploy.rb")
115
+ check_setting(:p4client, "Add set :p4client, <your client-spec name> to deploy.rb")
116
+ end
117
+
118
+ def check_setting(p4setting, message)
119
+ raise "#{p4setting} is not configured. #{message}" unless configuration[p4setting]
120
+ end
121
+
122
+ def p4_stream_handler(actor)
123
+ Proc.new do |ch, stream, out|
124
+ prefix = "#{stream} :: #{ch[:host]}"
125
+ actor.logger.info out, prefix
126
+ if out =~ /\(P4PASSWD\) invalid or unset\./i
127
+ raise "p4passwd is incorrect or unset"
128
+ elsif out =~ /Can.t create a new user.*/i
129
+ raise "p4user is incorrect or unset"
130
+ elsif out =~ /Perforce client error\:/i
131
+ raise "p4port is incorrect or unset"
132
+ elsif out =~ /Client \'[\w\-\_\.]+\' unknown.*/i
133
+ raise "p4client is incorrect or unset"
134
+ end
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,122 @@
1
+ require 'capistrano/scm/base'
2
+
3
+ module Capistrano
4
+ module SCM
5
+
6
+ # An SCM module for using subversion as your source control tool. This
7
+ # module is used by default, but you can explicitly specify it by
8
+ # placing the following line in your configuration:
9
+ #
10
+ # set :scm, :subversion
11
+ #
12
+ # Also, this module accepts a <tt>:svn</tt> configuration variable,
13
+ # which (if specified) will be used as the full path to the svn
14
+ # executable on the remote machine:
15
+ #
16
+ # set :svn, "/opt/local/bin/svn"
17
+ class Subversion < Base
18
+ # Return an integer identifying the last known revision in the svn
19
+ # repository. (This integer is currently the revision number.) If latest
20
+ # revision does not exist in the given repository, this routine will
21
+ # walk up the directory tree until it finds it.
22
+ def latest_revision
23
+ configuration.logger.debug "querying latest revision..." unless @latest_revision
24
+ repo = configuration.repository
25
+ until @latest_revision
26
+ match = svn_log(repo).scan(/r(\d+)/).first
27
+ @latest_revision = match ? match.first : nil
28
+ if @latest_revision.nil?
29
+ # if a revision number was not reported, move up a level in the path
30
+ # and try again.
31
+ repo = File.dirname(repo)
32
+ end
33
+ end
34
+ @latest_revision
35
+ end
36
+
37
+ # Return the number of the revision currently deployed.
38
+ def current_revision(actor)
39
+ latest = actor.releases.last
40
+ grep = %(grep " #{latest}$" #{configuration.deploy_to}/revisions.log)
41
+ result = ""
42
+ actor.run(grep, :once => true) do |ch, str, out|
43
+ result << out if str == :out
44
+ raise "could not determine current revision" if str == :err
45
+ end
46
+
47
+ date, time, user, rev, dir = result.split
48
+ raise "current revision not found in revisions.log" unless dir == latest
49
+
50
+ rev.to_i
51
+ end
52
+
53
+ # Return a string containing the diff between the two revisions. +from+
54
+ # and +to+ may be in any format that svn recognizes as a valid revision
55
+ # identifier. If +from+ is +nil+, it defaults to the last deployed
56
+ # revision. If +to+ is +nil+, it defaults to HEAD.
57
+ def diff(actor, from=nil, to=nil)
58
+ from ||= current_revision(actor)
59
+ to ||= "HEAD"
60
+
61
+ `svn diff #{configuration.repository}@#{from} #{configuration.repository}@#{to}`
62
+ end
63
+
64
+ # Check out (on all servers associated with the current task) the latest
65
+ # revision. Uses the given actor instance to execute the command. If
66
+ # svn asks for a password this will automatically provide it (assuming
67
+ # the requested password is the same as the password for logging into the
68
+ # remote server.)
69
+ def checkout(actor)
70
+ op = configuration[:checkout] || "co"
71
+ username = configuration[:svn_username] ? "--username #{configuration[:svn_username]}" : ""
72
+ command = "#{svn} #{op} #{username} -q -r#{configuration.revision} #{configuration.repository} #{actor.release_path} &&"
73
+ run_checkout(actor, command, &svn_stream_handler(actor))
74
+ end
75
+
76
+ # Update the current release in-place. This assumes that the original
77
+ # deployment was made using checkout, and not something like export.
78
+ def update(actor)
79
+ command = "cd #{actor.current_path} && #{svn} up -q &&"
80
+ run_update(actor, command, &svn_stream_handler(actor))
81
+ end
82
+
83
+ private
84
+
85
+ def svn
86
+ configuration[:svn] || "svn"
87
+ end
88
+
89
+ def svn_log(path)
90
+ `svn log -q -rhead #{path}`
91
+ end
92
+
93
+ def svn_password
94
+ configuration[:svn_password] || configuration[:password]
95
+ end
96
+
97
+ def svn_stream_handler(actor)
98
+ Proc.new do |ch, stream, out|
99
+ prefix = "#{stream} :: #{ch[:host]}"
100
+ actor.logger.info out, prefix
101
+ if out =~ /\bpassword.*:/i
102
+ actor.logger.info "subversion is asking for a password", prefix
103
+ ch.send_data "#{svn_password}\n"
104
+ elsif out =~ %r{\(yes/no\)}
105
+ actor.logger.info "subversion is asking whether to connect or not",
106
+ prefix
107
+ ch.send_data "yes\n"
108
+ elsif out =~ %r{passphrase}
109
+ message = "subversion needs your key's passphrase, sending empty string"
110
+ actor.logger.info message, prefix
111
+ ch.send_data "\n"
112
+ elsif out =~ %r{The entry \'(\w+)\' is no longer a directory}
113
+ message = "subversion can't update because directory '#{$1}' was replaced. Please add it to svn:ignore."
114
+ actor.logger.info message, prefix
115
+ raise message
116
+ end
117
+ end
118
+ end
119
+ end
120
+
121
+ end
122
+ end
@@ -0,0 +1,39 @@
1
+ require 'net/ssh'
2
+
3
+ module Capistrano
4
+ unless ENV['SKIP_VERSION_CHECK']
5
+ require 'capistrano/version'
6
+ require 'net/ssh/version'
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(".")}"
11
+ end
12
+ end
13
+
14
+ # A helper class for dealing with SSH connections.
15
+ class SSH
16
+ # An abstraction to make it possible to connect to the server via public key
17
+ # without prompting for the password. If the public key authentication fails
18
+ # this will fall back to password authentication.
19
+ #
20
+ # If a block is given, the new session is yielded to it, otherwise the new
21
+ # session is returned.
22
+ def self.connect(server, config, port=22, &block)
23
+ methods = [ %w(publickey hostbased), %w(password keyboard-interactive) ]
24
+ password_value = nil
25
+
26
+ begin
27
+ ssh_options = { :username => config.user,
28
+ :password => password_value,
29
+ :port => port,
30
+ :auth_methods => methods.shift }.merge(config.ssh_options)
31
+ Net::SSH.start(server,ssh_options,&block)
32
+ rescue Net::SSH::AuthenticationFailed
33
+ raise if methods.empty?
34
+ password_value = config.password
35
+ retry
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,90 @@
1
+ begin
2
+ require 'capistrano/version'
3
+ require 'net/sftp'
4
+ require 'net/sftp/version'
5
+ sftp_version = [Net::SFTP::Version::MAJOR, Net::SFTP::Version::MINOR, Net::SFTP::Version::TINY]
6
+ required_version = [1,1,0]
7
+ if !Capistrano::Version.check(required_version, sftp_version)
8
+ warn "You have Net::SFTP #{sftp_version.join(".")}, but you need at least #{required_version.join(".")}. Net::SFTP will not be used."
9
+ Capistrano::SFTP = false
10
+ else
11
+ Capistrano::SFTP = true
12
+ end
13
+ rescue LoadError
14
+ Capistrano::SFTP = false
15
+ end
16
+
17
+ module Capistrano
18
+
19
+ # This class encapsulates a single file transfer to be performed in parallel
20
+ # across multiple machines, using the SFTP protocol.
21
+ class Transfer
22
+ def initialize(servers, actor, filename, params={}) #:nodoc:
23
+ @servers = servers
24
+ @actor = actor
25
+ @filename = filename
26
+ @params = params
27
+ @completed = 0
28
+ @failed = 0
29
+ @sftps = setup_transfer
30
+ end
31
+
32
+ def logger #:nodoc:
33
+ @actor.logger
34
+ end
35
+
36
+ # Uploads to all specified servers in parallel.
37
+ def process!
38
+ logger.debug "uploading #{@filename}"
39
+
40
+ loop do
41
+ @sftps.each { |sftp| sftp.channel.connection.process(true) }
42
+ break if @completed == @servers.length
43
+ end
44
+
45
+ logger.trace "upload finished"
46
+
47
+ if @failed > 0
48
+ raise "upload of #{@filename} failed on one or more hosts"
49
+ end
50
+
51
+ self
52
+ end
53
+
54
+ private
55
+
56
+ def setup_transfer
57
+ @servers.map do |server|
58
+ sftp = @actor.sessions[server].sftp
59
+ sftp.connect unless sftp.state == :open
60
+
61
+ sftp.open(@filename, IO::WRONLY | IO::CREAT | IO::TRUNC, @params[:mode] || 0660) do |status, handle|
62
+ break unless check_status("open #{@filename}", server, status)
63
+
64
+ logger.info "uploading data to #{server}:#{@filename}"
65
+ sftp.write(handle, @params[:data] || "") do |status|
66
+ break unless check_status("write to #{server}:#{@filename}", server, status)
67
+ sftp.close_handle(handle) do
68
+ logger.debug "done uploading data to #{server}:#{@filename}"
69
+ @completed += 1
70
+ end
71
+ end
72
+ end
73
+
74
+ sftp
75
+ end
76
+ end
77
+
78
+ def check_status(action, server, status)
79
+ if status.code != Net::SFTP::Session::FX_OK
80
+ logger.error "could not #{action} on #{server} (#{status.message})"
81
+ @failed += 1
82
+ @completed += 1
83
+ return false
84
+ end
85
+
86
+ true
87
+ end
88
+ end
89
+
90
+ end
@@ -0,0 +1,26 @@
1
+ module Capistrano
2
+ # A helper method for converting a comma-delimited string into an array of
3
+ # roles.
4
+ def self.str2roles(string)
5
+ list = string.split(/,/).map { |s| s.strip.to_sym }
6
+ list.empty? ? nil : list
7
+ end
8
+
9
+ # Used by third-party task bundles to identify the capistrano configuration
10
+ # that is loading them. It's return value is not reliable in other contexts.
11
+ # If +require_config+ is not false, an exception will be raised if the current
12
+ # configuration is not set.
13
+ def self.configuration(require_config=false)
14
+ config = Thread.current[:capistrano_configuration]
15
+ if require_config && config.nil?
16
+ raise "Please require this file from within a Capistrano recipe"
17
+ end
18
+ config
19
+ end
20
+
21
+ # Used internally by Capistrano to specify the current configuration before
22
+ # loading a third-party task bundle.
23
+ def self.configuration=(config)
24
+ Thread.current[:capistrano_configuration] = config
25
+ end
26
+ end
@@ -0,0 +1,30 @@
1
+ module Capistrano
2
+ module Version #:nodoc:
3
+ # A method for comparing versions of required modules. It expects two
4
+ # arrays as parameters, and returns true if the first is no more than the
5
+ # second.
6
+ def self.check(expected, actual) #:nodoc:
7
+ good = false
8
+ if actual[0] > expected[0]
9
+ good = true
10
+ elsif actual[0] == expected[0]
11
+ if actual[1] > expected[1]
12
+ good = true
13
+ elsif actual[1] == expected[1] && actual[2] >= expected[2]
14
+ good = true
15
+ end
16
+ end
17
+
18
+ good
19
+ end
20
+
21
+ MAJOR = 1
22
+ MINOR = 1
23
+ TINY = 0
24
+
25
+ STRING = [MAJOR, MINOR, TINY].join(".")
26
+
27
+ SSH_REQUIRED = [1,0,8]
28
+ SFTP_REQUIRED = [1,1,0]
29
+ end
30
+ end