require 'capistrano/command' module Capistrano class Configuration module Actions module Invocation def self.included(base) #:nodoc: base.extend(ClassMethods) base.send :alias_method, :initialize_without_invocation, :initialize base.send :alias_method, :initialize, :initialize_with_invocation base.default_io_proc = Proc.new do |ch, stream, out| level = stream == :err ? :important : :info ch[:options][:logger].send(level, out, "#{stream} :: #{ch[:server]}") end end module ClassMethods attr_accessor :default_io_proc end def initialize_with_invocation(*args) #:nodoc: initialize_without_invocation(*args) set :default_environment, {} set :default_run_options, {} end # Executes different commands in parallel. This is useful for commands # that need to be different on different hosts, but which could be # otherwise run in parallel. # # The +options+ parameter is currently unused. # # Example: # # task :restart_everything do # parallel do |session| # session.when "in?(:app)", "/path/to/restart/mongrel" # session.when "in?(:web)", "/path/to/restart/apache" # session.when "in?(:db)", "/path/to/restart/mysql" # end # end # # Each command may have its own callback block, for capturing and # responding to output, with semantics identical to #run: # # session.when "in?(:app)", "/path/to/restart/mongrel" do |ch, stream, data| # # ch is the SSH channel for this command, used to send data # # back to the command (e.g. ch.send_data("password\n")) # # stream is either :out or :err, for which stream the data arrived on # # data is a string containing data sent from the remote command # end # # Also, you can specify a fallback command, to use when none of the # conditions match a server: # # session.else "/execute/something/else" # # The string specified as the first argument to +when+ may be any valid # Ruby code. It has access to the following variables and methods: # # * +in?(role)+ returns true if the server participates in the given role # * +server+ is the ServerDefinition object for the server. This can be # used to get the host-name, etc. # * +configuration+ is the current Capistrano::Configuration object, which # you can use to get the value of variables, etc. # # For example: # # session.when "server.host =~ /app/", "/some/command" # session.when "server.host == configuration[:some_var]", "/another/command" # session.when "in?(:web) || in?(:app)", "/more/commands" # # See #run for a description of the valid +options+. def parallel(options={}) raise ArgumentError, "parallel() requires a block" unless block_given? tree = Command::Tree.new(self) { |t| yield t } run_tree(tree, options) end # Invokes the given command. If a +via+ key is given, it will be used # to determine what method to use to invoke the command. It defaults # to :run, but may be :sudo, or any other method that conforms to the # same interface as run and sudo. def invoke_command(cmd, options={}, &block) options = options.dup via = options.delete(:via) || :run send(via, cmd, options, &block) 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 (:err for stderr, and :out for # stdout), and the data that was received. # # The +options+ hash may include any of the following keys: # # * :hosts - this is either a string (for a single target host) or an array # of strings, indicating which hosts the command should run on. By default, # the hosts are determined from the task definition. # * :roles - this is either a string or symbol (for a single target role) or # an array of strings or symbols, indicating which roles the command should # run on. If :hosts is specified, :roles will be ignored. # * :only - specifies a condition limiting which hosts will be selected to # run the command. This should refer to values set in the role definition. # For example, if a role is defined with :primary => true, then you could # select only hosts with :primary true by setting :only => { :primary => true }. # * :except - specifies a condition limiting which hosts will be selected to # run the command. This is the inverse of :only (hosts that do _not_ match # the condition will be selected). # * :on_no_matching_servers - if :continue, will continue to execute tasks if # no matching servers are found for the host criteria. The default is to raise # a NoMatchingServersError exception. # * :once - if true, only the first matching server will be selected. The default # is false (all matching servers will be selected). # * :max_hosts - specifies the maximum number of hosts that should be selected # at a time. If this value is less than the number of hosts that are selected # to run, then the hosts will be run in groups of max_hosts. The default is nil, # which indicates that there is no maximum host limit. Please note this does not # limit the number of SSH channels that can be open, only the number of hosts upon # which this will be called. # * :shell - says which shell should be used to invoke commands. This # defaults to "sh". Setting this to false causes Capistrano to invoke # the commands directly, without wrapping them in a shell invocation. # * :data - if not nil (the default), this should be a string that will # be passed to the command's stdin stream. # * :pty - if true, a pseudo-tty will be allocated for each command. The # default is false. Note that there are benefits and drawbacks both ways. # Empirically, it appears that if a pty is allocated, the SSH server daemon # will _not_ read user shell start-up scripts (e.g. bashrc, etc.). However, # if a pty is _not_ allocated, some commands will refuse to run in # interactive mode and will not prompt for (e.g.) passwords. # * :env - a hash of environment variable mappings that should be made # available to the command. The keys should be environment variable names, # and the values should be their corresponding values. The default is # empty, but may be modified by changing the +default_environment+ # Capistrano variable. # * :eof - if true, the standard input stream will be closed after sending # any data specified in the :data option. If false, the input stream is # left open. The default is to close the input stream only if no block is # passed. # # Note that if you set these keys in the +default_run_options+ Capistrano # variable, they will apply for all invocations of #run, #invoke_command, # and #parallel. def run(cmd, options={}, &block) if options[:eof].nil? && !cmd.include?(sudo) options = options.merge(:eof => !block_given?) end block ||= self.class.default_io_proc tree = Command::Tree.new(self) { |t| t.else(cmd, &block) } run_tree(tree, options) end # Executes a Capistrano::Command::Tree object. This is not for direct # use, but should instead be called indirectly, via #run or #parallel, # or #invoke_command. def run_tree(tree, options={}) #:nodoc: options = add_default_command_options(options) if tree.branches.any? || tree.fallback _, servers = filter_servers(options) branches = branches_for_servers(tree,servers) case branches.size when 0 branches = tree.branches.dup + [tree.fallback] branches.compact! case branches.size when 1 logger.debug "no servers for #{branches.first}" else logger.debug "no servers for commands" branches.each{ |branch| logger.trace "-> #{branch.to_s(true)}" } end when 1 logger.debug "executing #{branches.first}" unless options[:silent] else logger.debug "executing multiple commands in parallel" branches.each {|branch| logger.trace "-> #{branch.to_s(true)}" } end else raise ArgumentError, "attempt to execute without specifying a command" end return if dry_run || (debug && continue_execution(tree) == false) tree.each do |branch| if branch.command.include?(sudo) branch.callback = sudo_behavior_callback(branch.callback) end end execute_on_servers(options) do |servers| targets = servers.map { |s| sessions[s] } Command.process(tree, targets, options.merge(:logger => logger)) end end # Returns the command string used by capistrano to invoke a comamnd via # sudo. # # run "#{sudo :as => 'bob'} mkdir /path/to/dir" # # It can also be invoked like #run, but executing the command via sudo. # This assumes that the sudo password (if required) is the same as the # password for logging in to the server. # # sudo "mkdir /path/to/dir" # # Also, this method understands a :sudo 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" # # If you know what you're doing, you can also set :sudo_prompt, # which tells capistrano which prompt sudo should use when asking for # a password. (This is so that capistrano knows what prompt to look for # in the output.) If you set :sudo_prompt to an empty string, Capistrano # will not send a preferred prompt. def sudo(*parameters, &block) options = parameters.last.is_a?(Hash) ? parameters.pop.dup : {} command = parameters.first user = options[:as] && "-u #{options.delete(:as)}" sudo_prompt_option = "-p '#{sudo_prompt}'" unless sudo_prompt.empty? sudo_command = [fetch(:sudo, "sudo"), sudo_prompt_option, user].compact.join(" ") if command command = sudo_command + " " + command run(command, options, &block) else return sudo_command end 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.to_s =~ /^Sorry, try again/ if prompt_host.nil? || prompt_host == ch[:server] prompt_host = ch[:server] logger.important out, "#{stream} :: #{ch[:server]}" reset! :password end end if out.to_s =~ /^#{Regexp.escape(sudo_prompt)}/ ch.send_data "#{self[:password]}\n" elsif fallback fallback.call(ch, stream, out) end end end # Merges the various default command options into the options hash and # returns the result. The default command options that are understand # are: # # * :default_environment: If the :env key already exists, the :env # key is merged into default_environment and then added back into # options. # * :default_shell: if the :shell key already exists, it will be used. # Otherwise, if the :default_shell key exists in the configuration, # it will be used. Otherwise, no :shell key is added. def add_default_command_options(options) defaults = self[:default_run_options] options = defaults.merge(options) env = self[:default_environment] env = env.merge(options[:env]) if options[:env] options[:env] = env unless env.empty? shell = options[:shell] || self[:default_shell] options[:shell] = shell unless shell.nil? options end # Returns the prompt text to use with sudo def sudo_prompt fetch(:sudo_prompt, "sudo password: ") end def continue_execution(tree) if tree.branches.length == 1 continue_execution_for_branch(tree.branches.first) else tree.each { |branch| branch.skip! unless continue_execution_for_branch(branch) } tree.any? { |branch| !branch.skip? } end end def continue_execution_for_branch(branch) case Capistrano::CLI.debug_prompt(branch) when "y" true when "n" false when "a" exit(-1) end end private def branches_for_servers(tree,servers) servers.inject([]) do |branches,server| if server_branches = tree.branches_for(server) branches += server_branches end branches end.compact.uniq end end end end end