module Fastlane module Actions module SharedValues SSH_STDOUT_VALUE = :SSH_STDOUT_VALUE SSH_STDERR_VALUE = :SSH_STDERR_VALUE end class SshAction < Action def self.ssh_exec!(ssh, command, log = true) stdout_data = "" stderr_data = "" exit_code = nil exit_signal = nil ssh.open_channel do |channel| channel.exec(command) do |ch, success| unless success abort("FAILED: couldn't execute command (ssh.channel.exec)") end channel.on_data do |ch1, data| stdout_data += data UI.command_output(data) if log end channel.on_extended_data do |ch2, type, data| # Only type 1 data is stderr (though no other types are defined by the standard) # See http://net-ssh.github.io/net-ssh/Net/SSH/Connection/Channel.html#method-i-on_extended_data stderr_data += data if type == 1 end channel.on_request("exit-status") do |ch3, data| exit_code = data.read_long end channel.on_request("exit-signal") do |ch4, data| exit_signal = data.read_long end end end # Wait for all open channels to close ssh.loop { stdout: stdout_data, stderr: stderr_data, exit_code: exit_code, exit_signal: exit_signal } end def self.run(params) Actions.verify_gem!('net-ssh') require "net/ssh" Actions.lane_context[SharedValues::SSH_STDOUT_VALUE] = "" Actions.lane_context[SharedValues::SSH_STDERR_VALUE] = "" stdout = "" stderr = "" Net::SSH.start(params[:host], params[:username], { port: params[:port].to_i, password: params[:password] }) do |ssh| params[:commands].each do |cmd| UI.command(cmd) if params[:log] return_value = ssh_exec!(ssh, cmd, params[:log]) if return_value[:exit_code] != 0 UI.error("SSH Command failed '#{cmd}' Exit-Code: #{return_value[:exit_code]}") UI.user_error!("SSH Command failed") end stderr << return_value[:stderr] stdout << return_value[:stdout] end end command_word = params[:commands].count == 1 ? "command" : "commands" UI.success("Successfully executed #{params[:commands].count} #{command_word} on host #{params[:host]}") Actions.lane_context[SharedValues::SSH_STDOUT_VALUE] = stdout Actions.lane_context[SharedValues::SSH_STDERR_VALUE] = stderr return { stdout: Actions.lane_context[SharedValues::SSH_STDOUT_VALUE], stderr: Actions.lane_context[SharedValues::SSH_STDERR_VALUE] } end ##################################################### # @!group Documentation ##################################################### def self.description "Allows remote command execution using ssh" end def self.details "Lets you execute remote commands via ssh using username/password or ssh-agent. If one of the commands in command-array returns non 0, it fails." end def self.available_options [ FastlaneCore::ConfigItem.new(key: :username, short_option: "-u", env_name: "FL_SSH_USERNAME", description: "Username"), FastlaneCore::ConfigItem.new(key: :password, short_option: "-p", env_name: "FL_SSH_PASSWORD", sensitive: true, description: "Password", optional: true), FastlaneCore::ConfigItem.new(key: :host, short_option: "-H", env_name: "FL_SSH_HOST", description: "Hostname"), FastlaneCore::ConfigItem.new(key: :port, short_option: "-P", env_name: "FL_SSH_PORT", description: "Port", optional: true, default_value: "22"), FastlaneCore::ConfigItem.new(key: :commands, short_option: "-C", env_name: "FL_SSH_COMMANDS", description: "Commands", optional: true, type: Array), FastlaneCore::ConfigItem.new(key: :log, short_option: "-l", env_name: "FL_SSH_LOG", description: "Log commands and output", optional: true, default_value: true, type: Boolean) ] end def self.output [ ['SSH_STDOUT_VALUE', 'Holds the standard output of all commands'], ['SSH_STDERR_VALUE', 'Holds the standard error of all commands'] ] end def self.authors ["hjanuschka"] end def self.is_supported?(platform) true end def self.example_code [ 'ssh( host: "dev.januschka.com", username: "root", commands: [ "date", "echo 1 > /tmp/file1" ] )' ] end def self.category :misc end end end end