Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

start refactoring the task helper actions

git-svn-id: http://svn.rubyonrails.org/rails/tools/capistrano@6288 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
  • Loading branch information...
commit 472431004c5ffde7d1b0d2127269e98c6c05da03 1 parent acdd65f
@jamis jamis authored
View
1  Rakefile
@@ -19,7 +19,6 @@ desc "Build documentation"
task :doc => [ :rdoc ]
Rake::TestTask.new do |t|
- t.ruby_opts << "-rubygems"
t.test_files = Dir["test/**/*_test.rb"]
t.verbose = true
end
View
158 lib/capistrano/actor.rb
@@ -13,42 +13,6 @@ module Capistrano
# directly--rather, you create a new Configuration instance, and access the
# new actor via Configuration#actor.
class Actor
- class <<self
- attr_accessor :default_io_proc
- end
-
- self.default_io_proc = Proc.new do |ch, stream, out|
- level = stream == :err ? :important : :info
- ch[:actor].logger.send(level, out, "#{stream} :: #{ch[:host]}")
- end
-
- def initialize(config) #:nodoc:
- @configuration = config
- @tasks = {}
- @task_call_frames = []
- @sessions = {}
- @factory = self.class.connection_factory.new(configuration)
- end
-
- # Execute the given command on all servers that are the target of the
- # current task. If a block is given, it is invoked for all output
- # generated by the command, and should accept three parameters: the SSH
- # channel (which may be used to send data back to the remote process),
- # the stream identifier (<tt>:err</tt> for stderr, and <tt>:out</tt> for
- # stdout), and the data that was received.
- #
- # If +pretend+ mode is active, this does nothing.
- def run(cmd, options={}, &block)
- block ||= default_io_proc
- logger.debug "executing #{cmd.strip.inspect}"
-
- execute_on_servers(options) do |servers|
- # execute the command on each server in parallel
- command = self.class.command_factory.new(servers, cmd, block, options, self)
- command.process! # raises an exception if command fails on any server
- end
- end
-
# Streams the result of the command from all servers that are the target of the
# current task. All these streams will be joined into a single one,
# so you can, say, watch 10 log files as though they were one. Do note that this
@@ -78,56 +42,6 @@ def delete(path, options={})
run(cmd, options)
end
- # Store the given data at the given location on all servers targetted by
- # the current task. If <tt>:mode</tt> is specified it is used to set the
- # mode on the file.
- def put(data, path, options={})
- if Capistrano::SFTP
- execute_on_servers(options) do |servers|
- transfer = self.class.transfer_factory.new(servers, self, path, :data => data,
- :mode => options[:mode])
- transfer.process!
- end
- else
- # Poor-man's SFTP... just run a cat on the remote end, and send data
- # to it.
-
- cmd = "cat > #{path}"
- cmd << " && chmod #{options[:mode].to_s(8)} #{path}" if options[:mode]
- run(cmd, options.merge(:data => data + "\n\4")) do |ch, stream, out|
- logger.important out, "#{stream} :: #{ch[:host]}" if stream == :err
- end
- end
- end
-
- # Get file remote_path from FIRST server targetted by
- # the current task and transfer it to local machine as path. It will use
- # SFTP if Net::SFTP is installed; otherwise it will fall back to using
- # 'cat', which may cause corruption in binary files.
- #
- # get "#{deploy_to}/current/log/production.log", "log/production.log.web"
- def get(remote_path, path, options = {})
- if Capistrano::SFTP && options.fetch(:sftp, true)
- execute_on_servers(options.merge(:once => true)) do |servers|
- logger.debug "downloading #{servers.first}:#{remote_path} to #{path}"
- sftp = sessions[servers.first].sftp
- sftp.connect unless sftp.state == :open
- sftp.get_file remote_path, path
- logger.trace "download finished"
- end
- else
- logger.important "Net::SFTP is not available; using remote 'cat' to get file, which may cause file corruption"
- File.open(path, "w") do |destination|
- run "cat #{remote_path}", :once => true do |ch, stream, data|
- case stream
- when :out then destination << data
- when :err then raise "error while downloading #{remote_path}: #{data.inspect}"
- end
- end
- end
- end
- end
-
# Executes the given command on the first server targetted by the current
# task, collects it's stdout into a string, and returns the string.
def capture(command, options={})
@@ -141,42 +55,6 @@ def capture(command, options={})
output
end
- # Like #run, but executes the command via <tt>sudo</tt>. This assumes that
- # the sudo password (if required) is the same as the password for logging
- # in to the server.
- #
- # Also, this module accepts a <tt>:sudo</tt> configuration variable,
- # which (if specified) will be used as the full path to the sudo
- # executable on the remote machine:
- #
- # set :sudo, "/opt/local/bin/sudo"
- def sudo(command, options={}, &block)
- block ||= default_io_proc
-
- # in order to prevent _each host_ from prompting when the password was
- # wrong, let's track which host prompted first and only allow subsequent
- # prompts from that host.
- prompt_host = nil
- user = options[:as].nil? ? '' : "-u #{options[:as]}"
-
- run "#{sudo_command} #{user} #{command}", options do |ch, stream, out|
- if out =~ /^Password:/
- ch.send_data "#{password}\n"
- elsif out =~ /try again/
- if prompt_host.nil? || prompt_host == ch[:host]
- prompt_host = ch[:host]
- logger.important out, "#{stream} :: #{ch[:host]}"
- # reset the password to it's original value and prepare for another
- # pass (the reset allows the password prompt to be attempted again
- # if the password variable was originally a proc (the default)
- set :password, self[:original_value][:password] || self[:password]
- end
- else
- block.call(ch, stream, out)
- end
- end
- end
-
# Renders an ERb template and returns the result. This is useful for
# dynamically building documents to store on the remote servers.
#
@@ -230,41 +108,5 @@ def render(*args)
raise ArgumentError, "no file or template given for rendering"
end
end
-
- # An instance-level reader for the class' #default_io_proc attribute.
- def default_io_proc
- self.class.default_io_proc
- end
-
- # Used to force connections to be made to the current task's servers.
- # Connections are normally made lazily in Capistrano--you can use this
- # to force them open before performing some operation that might be
- # time-sensitive.
- def connect!(options={})
- execute_on_servers(options) { }
- end
-
- def metaclass
- class << self; self; end
- end
-
- private
-
- def sudo_command
- configuration[:sudo] || "sudo"
- end
-
- def define_method(name, &block)
- metaclass.send(:define_method, name, &block)
- end
-
- def method_missing(sym, *args, &block)
- if @configuration.respond_to?(sym)
- @configuration.send(sym, *args, &block)
- else
- super
- end
- end
-
end
end
View
23 lib/capistrano/command.rb
@@ -7,7 +7,20 @@ class Error < RuntimeError; end
attr_reader :command, :sessions, :options
- def initialize(command, sessions, options={}, &block) #:nodoc:
+ def self.process(command, sessions, options={}, &block)
+ new(command, sessions, options, &block).process!
+ end
+
+ # Instantiates a new command object. The +command+ must be a string
+ # containing the command to execute. +sessions+ is an array of Net::SSH
+ # session instances, and +options+ must be a hash containing any of the
+ # following keys:
+ #
+ # * +logger+: (optional), a Capistrano::Logger instance
+ # * +data+: (optional), a string to be sent to the command via it's stdin
+ # * +env+: (optional), a string or hash to be interpreted as environment
+ # variables that should be defined for this command invocation.
+ def initialize(command, sessions, options={}, &block)
@command = extract_environment(options) + command.strip.gsub(/\r?\n/, "\\\n")
@sessions = sessions
@options = options
@@ -15,10 +28,6 @@ def initialize(command, sessions, options={}, &block) #:nodoc:
@channels = open_channels
end
- def logger #:nodoc:
- options[:logger]
- end
-
# Processes the command in parallel on all specified hosts. If the command
# fails (non-zero return code) on any of the hosts, this will raise a
# RuntimeError.
@@ -59,6 +68,10 @@ def stop!
private
+ def logger
+ options[:logger]
+ end
+
def open_channels
sessions.map do |session|
session.open_channel do |channel|
View
6 lib/capistrano/configuration.rb
@@ -1,5 +1,6 @@
require 'capistrano/logger'
require 'capistrano/extensions'
+
require 'capistrano/configuration/connections'
require 'capistrano/configuration/execution'
require 'capistrano/configuration/loading'
@@ -7,6 +8,8 @@
require 'capistrano/configuration/roles'
require 'capistrano/configuration/variables'
+require 'capistrano/configuration/actions/invocation'
+
module Capistrano
# Represents a specific Capistrano configuration. A Configuration instance
# may be used to load multiple recipe files, define and describe tasks,
@@ -22,5 +25,8 @@ def initialize #:nodoc:
# The includes must come at the bottom, since they may redefine methods
# defined in the base class.
include Connections, Execution, Loading, Namespaces, Roles, Variables
+
+ # Mix in the actions
+ include Actions::FileTransfer, Actions::Invocation
end
end
View
37 lib/capistrano/configuration/actions/file_transfer.rb
@@ -0,0 +1,37 @@
+require 'capistrano/upload'
+
+module Capistrano
+ class Configuration
+ module Actions
+ module FileTransfer
+
+ # Store the given data at the given location on all servers targetted
+ # by the current task. If <tt>:mode</tt> is specified it is used to
+ # set the mode on the file.
+ def put(data, path, options={})
+ execute_on_servers(options) do |servers|
+ targets = servers.map { |s| sessions[s.host] }
+ Upload.process(targets, path, :data => data, :mode => options[:mode], :logger => logger)
+ end
+ end
+
+ # Get file remote_path from FIRST server targetted by
+ # the current task and transfer it to local machine as path. It will use
+ # SFTP if Net::SFTP is installed; otherwise it will fall back to using
+ # 'cat', which may cause corruption in binary files.
+ #
+ # get "#{deploy_to}/current/log/production.log", "log/production.log.web"
+ def get(remote_path, path, options = {})
+ execute_on_servers(options.merge(:once => true)) do |servers|
+ logger.info "downloading `#{servers.first.host}:#{remote_path}' to `#{path}'"
+ sftp = sessions[servers.first.host].sftp
+ sftp.connect unless sftp.state == :open
+ sftp.get_file remote_path, path
+ logger.debug "download finished"
+ end
+ end
+
+ end
+ end
+ end
+end
View
86 lib/capistrano/configuration/actions/invocation.rb
@@ -0,0 +1,86 @@
+require 'capistrano/command'
+
+module Capistrano
+ class Configuration
+ module Actions
+ module Invocation
+ def self.included(base)
+ base.extend(ClassMethods)
+
+ base.default_io_proc = Proc.new do |ch, stream, out|
+ level = stream == :err ? :important : :info
+ ch[:options][:logger].send(level, out, "#{stream} :: #{ch[:host]}")
+ end
+ end
+
+ module ClassMethods
+ attr_accessor :default_io_proc
+ end
+
+ # Execute the given command on all servers that are the target of the
+ # current task. If a block is given, it is invoked for all output
+ # generated by the command, and should accept three parameters: the SSH
+ # channel (which may be used to send data back to the remote process),
+ # the stream identifier (<tt>:err</tt> for stderr, and <tt>:out</tt> for
+ # stdout), and the data that was received.
+ #
+ # If +pretend+ mode is active, this does nothing.
+ def run(cmd, options={}, &block)
+ block ||= self.class.default_io_proc
+ logger.debug "executing #{cmd.strip.inspect}"
+
+ execute_on_servers(options) do |servers|
+ targets = servers.map { |s| sessions[s.host] }
+ Command.process(cmd, targets, options.merge(:logger => logger), &block)
+ end
+ end
+
+ # Like #run, but executes the command via <tt>sudo</tt>. This assumes
+ # that the sudo password (if required) is the same as the password for
+ # logging in to the server.
+ #
+ # Also, this module accepts a <tt>:sudo</tt> configuration variable,
+ # which (if specified) will be used as the full path to the sudo
+ # executable on the remote machine:
+ #
+ # set :sudo, "/opt/local/bin/sudo"
+ def sudo(command, options={}, &block)
+ block ||= self.class.default_io_proc
+
+ options = options.dup
+ as = options.delete(:as)
+
+ user = as && "-u #{as}"
+ command = [fetch(:sudo, "sudo"), user, command].compact.join(" ")
+
+ run(command, options, &sudo_behavior_callback(block))
+ end
+
+ # Returns a Proc object that defines the behavior of the sudo
+ # callback. The returned Proc will defer to the +fallback+ argument
+ # (which should also be a Proc) for any output it does not
+ # explicitly handle.
+ def sudo_behavior_callback(fallback) #:nodoc:
+ # in order to prevent _each host_ from prompting when the password
+ # was wrong, let's track which host prompted first and only allow
+ # subsequent prompts from that host.
+ prompt_host = nil
+
+ Proc.new do |ch, stream, out|
+ if out =~ /^Password:/
+ ch.send_data "#{self[:password]}\n"
+ elsif out =~ /try again/
+ if prompt_host.nil? || prompt_host == ch[:host]
+ prompt_host = ch[:host]
+ logger.important out, "#{stream} :: #{ch[:host]}"
+ reset! :password
+ end
+ else
+ fallback.call(ch, stream, out)
+ end
+ end
+ end
+ end
+ end
+ end
+end
View
4 lib/capistrano/upload.rb
@@ -30,6 +30,10 @@ class Upload
# the requested data.
class Error < RuntimeError; end
+ def self.process(sessions, filename, options)
+ new(sessions, filename, options).process!
+ end
+
attr_reader :sessions, :filename, :options
attr_reader :failed, :completed
View
4 lib/capistrano/utils.rb
@@ -11,14 +11,14 @@ def self.str2roles(string)
# If +require_config+ is not false, an exception will be raised if the current
# configuration is not set.
def self.configuration(require_config=false)
- warn "[DEPRECATION] please use Capistrano::Configuration.instance instead of Capistrano.configuration. (You may be using a Capistrano plugin that is using this deprecated syntax.)"
+ warn "[DEPRECATION] use Capistrano::Configuration.instance instead of Capistrano.configuration. (You may be using a Capistrano plugin that is using this deprecated syntax.)"
Capistrano::Configuration.instance(require_config)
end
# Used internally by Capistrano to specify the current configuration before
# loading a third-party task bundle.
def self.configuration=(config)
- warn "[DEPRECATION] please us Capistrano::Configuration.instance= instead of Capistrano.configuration=."
+ warn "[DEPRECATION] use Capistrano::Configuration.instance= instead of Capistrano.configuration=."
Capistrano::Configuration.instance = config
end
end
View
4 lib/capistrano/version.rb
@@ -19,8 +19,8 @@ def self.check(expected, actual) #:nodoc:
end
MAJOR = 1
- MINOR = 4
- TINY = 1
+ MINOR = 99
+ TINY = 0
STRING = [MAJOR, MINOR, TINY].join(".")
View
38 test/configuration/actions/file_transfer_test.rb
@@ -0,0 +1,38 @@
+require "#{File.dirname(__FILE__)}/../../utils"
+require 'capistrano/configuration/actions/file_transfer'
+
+class ConfigurationActionsFileTransferTest < Test::Unit::TestCase
+ class MockConfig
+ include Capistrano::Configuration::Actions::FileTransfer
+ end
+
+ def setup
+ @config = MockConfig.new
+ @config.stubs(:logger).returns(stub_everything)
+ end
+
+ def test_put_should_pass_options_to_execute_on_servers
+ @config.expects(:execute_on_servers).with(:foo => "bar")
+ @config.put("some data", "test.txt", :foo => "bar")
+ end
+
+ def test_put_should_delegate_to_Upload_process
+ @config.expects(:execute_on_servers).yields(%w(s1 s2 s3).map { |s| mock(:host => s) })
+ @config.expects(:sessions).times(3).returns(Hash.new{|h,k| h[k] = k.to_sym})
+ Capistrano::Upload.expects(:process).with([:s1,:s2,:s3], "test.txt", :data => "some data", :mode => 0777, :logger => @config.logger)
+ @config.put("some data", "test.txt", :mode => 0777)
+ end
+
+ def test_get_should_pass_options_execute_on_servers_including_once
+ @config.expects(:execute_on_servers).with(:foo => "bar", :once => true)
+ @config.get("test.txt", "test.txt", :foo => "bar")
+ end
+
+ def test_get_should_use_sftp_get_file_to_local_path
+ sftp = mock("sftp", :state => :closed, :connect => true)
+ sftp.expects(:get_file).with("remote.txt", "local.txt")
+ @config.expects(:execute_on_servers).yields([stub("server", :host => "capistrano")])
+ @config.expects(:sessions).returns("capistrano" => mock("session", :sftp => sftp))
+ @config.get("remote.txt", "local.txt")
+ end
+end
View
144 test/configuration/actions/invocation_test.rb
@@ -0,0 +1,144 @@
+require "#{File.dirname(__FILE__)}/../../utils"
+require 'capistrano/configuration/actions/invocation'
+
+class ConfigurationActionsRunTest < Test::Unit::TestCase
+ class MockConfig
+ attr_reader :options
+
+ def initialize
+ @options = {}
+ end
+
+ def [](*args)
+ @options[*args]
+ end
+
+ def fetch(*args)
+ @options.fetch(*args)
+ end
+
+ include Capistrano::Configuration::Actions::Invocation
+ end
+
+ def setup
+ @config = MockConfig.new
+ @original_io_proc = MockConfig.default_io_proc
+ @config.stubs(:logger).returns(stub_everything)
+ end
+
+ def teardown
+ MockConfig.default_io_proc = @original_io_proc
+ end
+
+ def test_run_options_should_be_passed_to_execute_on_servers
+ @config.expects(:execute_on_servers).with(:foo => "bar")
+ @config.run "ls", :foo => "bar"
+ end
+
+ def test_run_without_block_should_use_default_io_proc
+ @config.expects(:execute_on_servers).yields(%w(s1 s2 s3).map { |s| mock(:host => s) })
+ @config.expects(:sessions).returns(Hash.new { |h,k| h[k] = k.to_sym }).times(3)
+ prepare_command("ls", [:s1, :s2, :s3], {:logger => @config.logger})
+ MockConfig.default_io_proc = inspectable_proc
+ @config.run "ls"
+ end
+
+ def test_run_with_block_should_use_block
+ @config.expects(:execute_on_servers).yields(%w(s1 s2 s3).map { |s| mock(:host => s) })
+ @config.expects(:sessions).returns(Hash.new { |h,k| h[k] = k.to_sym }).times(3)
+ prepare_command("ls", [:s1, :s2, :s3], {:logger => @config.logger})
+ MockConfig.default_io_proc = Proc.new { |a,b,c| raise "shouldn't get here" }
+ @config.run("ls", &inspectable_proc)
+ end
+
+ def test_default_io_proc_should_log_stdout_arguments_as_info
+ ch = { :host => "capistrano",
+ :options => { :logger => mock("logger") } }
+ ch[:options][:logger].expects(:info).with("data stuff", "out :: capistrano")
+ MockConfig.default_io_proc[ch, :out, "data stuff"]
+ end
+
+ def test_default_io_proc_should_log_stderr_arguments_as_important
+ ch = { :host => "capistrano",
+ :options => { :logger => mock("logger") } }
+ ch[:options][:logger].expects(:important).with("data stuff", "err :: capistrano")
+ MockConfig.default_io_proc[ch, :err, "data stuff"]
+ end
+
+ def test_sudo_should_default_to_sudo
+ @config.expects(:run).with("sudo ls", {})
+ @config.sudo "ls"
+ end
+
+ def test_sudo_should_use_sudo_variable_definition
+ @config.expects(:run).with("/opt/local/bin/sudo ls", {})
+ @config.options[:sudo] = "/opt/local/bin/sudo"
+ @config.sudo "ls"
+ end
+
+ def test_sudo_should_interpret_as_option_as_user
+ @config.expects(:run).with("sudo -u app ls", {})
+ @config.sudo "ls", :as => "app"
+ end
+
+ def test_sudo_should_pass_options_through_to_run
+ @config.expects(:run).with("sudo ls", :foo => "bar")
+ @config.sudo "ls", :foo => "bar"
+ end
+
+ def test_sudo_behavior_callback_should_send_password_when_prompted
+ ch = mock("channel")
+ ch.expects(:send_data).with("g00b3r\n")
+ @config.options[:password] = "g00b3r"
+ @config.sudo_behavior_callback(nil)[ch, nil, "Password: "]
+ end
+
+ def test_sudo_behavior_callback_with_incorrect_password_on_first_prompt
+ ch = mock("channel")
+ ch.stubs(:[]).with(:host).returns("capistrano")
+ @config.expects(:reset!).with(:password)
+ @config.sudo_behavior_callback(nil)[ch, nil, "blah blah try again blah blah"]
+ end
+
+ def test_sudo_behavior_callback_with_incorrect_password_on_subsequent_prompts
+ callback = @config.sudo_behavior_callback(nil)
+
+ ch = mock("channel")
+ ch.stubs(:[]).with(:host).returns("capistrano")
+ ch2 = mock("channel")
+ ch2.stubs(:[]).with(:host).returns("cap2")
+
+ @config.expects(:reset!).with(:password).times(2)
+
+ callback[ch, nil, "blah blah try again blah blah"]
+ callback[ch2, nil, "blah blah try again blah blah"] # shouldn't call reset!
+ callback[ch, nil, "blah blah try again blah blah"]
+ end
+
+ def test_sudo_behavior_callback_should_defer_to_fallback_for_other_output
+ callback = @config.sudo_behavior_callback(inspectable_proc)
+
+ a = mock("channel", :called => true)
+ b = mock("stream", :called => true)
+ c = mock("data", :called => true)
+
+ callback[a, b, c]
+ end
+
+ private
+
+ def inspectable_proc
+ Proc.new do |ch, stream, data|
+ ch.called
+ stream.called
+ data.called
+ end
+ end
+
+ def prepare_command(command, sessions, options)
+ a = mock("channel", :called => true)
+ b = mock("stream", :called => true)
+ c = mock("data", :called => true)
+ Capistrano::Command.expects(:process).with(command, sessions, options).yields(a, b, c)
+ end
+end
View
3  test/configuration_test.rb
@@ -1,4 +1,7 @@
require "#{File.dirname(__FILE__)}/utils"
+# if the following is uncommented, the capistrano gem gets loaded if it is
+# installed, for some reason...not sure why :(
+# require 'capistrano/configuration'
class ConfigurationTest < Test::Unit::TestCase
def setup
View
5 test/upload_test.rb
@@ -25,6 +25,11 @@ def test_initialize_should_get_sftp_for_each_session
Capistrano::Upload.new(sessions, "test.txt", :data => "data")
end
+ def test_self_process_should_instantiate_uploader_and_start_process
+ Capistrano::Upload.expects(:new).with([:s1, :s2], "test.txt", :data => "data").returns(mock(:process! => nil))
+ Capistrano::Upload.process([:s1, :s2], "test.txt", :data => "data")
+ end
+
def test_process_when_sftp_open_fails_should_raise_error
channel = mock("channel")
channel.expects(:[]=).with(:done, true)
Please sign in to comment.
Something went wrong with that request. Please try again.