diff options
| author | Micah Anderson <micah@riseup.net> | 2016-11-04 10:54:28 -0400 | 
|---|---|---|
| committer | Micah Anderson <micah@riseup.net> | 2016-11-04 10:54:28 -0400 | 
| commit | 34a381efa8f6295080c843f86bfa07d4e41056af (patch) | |
| tree | 9282cf5d4c876688602705a7fa0002bc4a810bde /lib/leap_cli/ssh/backend.rb | |
| parent | 0a72bc6fd292bf9367b314fcb0347c4d35042f16 (diff) | |
| parent | 5821964ff7e16ca7aa9141bd09a77d355db492a9 (diff) | |
Merge branch 'develop'
Diffstat (limited to 'lib/leap_cli/ssh/backend.rb')
| -rw-r--r-- | lib/leap_cli/ssh/backend.rb | 209 | 
1 files changed, 209 insertions, 0 deletions
| diff --git a/lib/leap_cli/ssh/backend.rb b/lib/leap_cli/ssh/backend.rb new file mode 100644 index 00000000..3894d815 --- /dev/null +++ b/lib/leap_cli/ssh/backend.rb @@ -0,0 +1,209 @@ +# +# A custome SSHKit backend, derived from the default netssh backend. +# Our custom backend modifies the logging behavior and gracefully captures +# common exceptions. +# + +require 'stringio' +require 'timeout' +require 'sshkit' +require 'leap_cli/ssh/formatter' +require 'leap_cli/ssh/scripts' + +module SSHKit +  class Command +    # +    # override exit_status in order to be less verbose +    # +    def exit_status=(new_exit_status) +      @finished_at = Time.now +      @exit_status = new_exit_status +      if options[:raise_on_non_zero_exit] && exit_status > 0 +        message = "" +        message += "exit status: " + exit_status.to_s + "\n" +        message += "stdout: " + (full_stdout.strip.empty? ? "Nothing written" : full_stdout.strip) + "\n" +        message += "stderr: " + (full_stderr.strip.empty? ? 'Nothing written' : full_stderr.strip) + "\n" +        raise Failed, message +      end +    end +  end +end + +module LeapCli +  module SSH +    class Backend < SSHKit::Backend::Netssh + +      # since the @pool is a class instance variable, we need to copy +      # the code from the superclass that initializes it. boo +      @pool = SSHKit::Backend::ConnectionPool.new + +      # modify to pass itself to the block, instead of relying on instance_exec. +      def run +        Thread.current["sshkit_backend"] = self +        # was: instance_exec(@host, &@block) +        @block.call(self, @host) +      ensure +        Thread.current["sshkit_backend"] = nil +      end + +      # if set, all the commands will begin with: +      # sudo -u #{@user} -- sh -c '<command>' +      def set_user(user='root') +        @user = user +      end + +      # +      # like default capture, but gracefully logs failures for us +      # last argument can be an options hash. +      # +      # available options: +      # +      #   :fail_msg    - [nil] if set, log this instead of the default +      #                  fail message. +      # +      #   :raise_error - [nil] if true, then reraise failed command exception. +      # +      #   :log_cmd     - [false] if true, log what the command is that gets run. +      # +      #   :log_output  - [true] if true, log each output from the command as +      #                  it is received. +      # +      #   :log_finish  - [false] if true, log the exit status and time +      #                  to completion +      # +      #   :log_wrap    - [nil] passed to log method as :wrap option. +      # +      def capture(*args) +        extract_options(args) +        initialize_logger(:log_output => false) +        rescue_ssh_errors(*args) do +          return super(*args) +        end +      end + +      # +      # like default execute, but log the results as they come in. +      # +      # see capture() for available options +      # +      def stream(*args) +        extract_options(args) +        initialize_logger +        rescue_ssh_errors(*args) do +          execute(*args) +        end +      end + +      def log(*args, &block) +        @logger ||= LeapCli.new_logger +        @logger.log(*args, &block) +      end + +      # some prewritten servers-side scripts +      def scripts +        @scripts ||= LeapCli::SSH::Scripts.new(self, @host.hostname) +      end + +      # +      # sshkit just passes upload! and download! to Net::SCP, but Net::SCP +      # make it impossible to set the file permissions. Here is how the mode +      # is determined, from upload.rb: +      # +      #    mode = channel[:stat] ? channel[:stat].mode & 07777 : channel[:options][:mode] +      # +      # The stat info from the file always overrides the mode you pass in options. +      # However, the channel[:options][:mode] will be applied for pure in-memory +      # uploads. So, if the mode is set, we convert the upload to be a memory +      # upload instead of a file upload. +      # +      # Stupid, but blame Net::SCP. +      # +      def upload!(src, dest, options={}) +        if options[:mode] +          if src.is_a?(StringIO) +            content = src +          else +            content = StringIO.new(File.read(src)) +          end +          super(content, dest, options) +        else +          super(src, dest, options) +        end +      end + +      private + +      # +      # creates a new logger instance for this specific ssh command. +      # by doing this, each ssh session has its own logger and its own +      # indentation. +      # +      # potentially modifies 'args' array argument. +      # +      def initialize_logger(default_options={}) +        @logger ||= LeapCli.new_logger +        @output = LeapCli::SSH::Formatter.new(@logger, @host, default_options.merge(@options)) +      end + +      def extract_options(args) +        if args.last.is_a? Hash +          @options = args.pop +        else +          @options = {} +        end +      end + +      # +      # capture common exceptions +      # +      def rescue_ssh_errors(*args, &block) +        yield +      rescue Net::SSH::HostKeyMismatch => exc +        @logger.log(:fatal_error, "Host key mismatch!") do +          @logger.log(exc.to_s) +          @logger.log("The ssh host key for the server does not match what is on "+ +            " file in `%s`." % Path.named_path(:known_hosts)) +          @logger.log("One of these is happening:") do +            @logger.log("There is an active Man in The Middle attack against you.") +            @logger.log("Or, someone has generated new host keys for the server " + +               "and your provider files are out of date.") +            @logger.log("Or, a new server is using this IP address " + +               "and your provider files are out of date.") +            @logger.log("Or, the server configuration has changed to use a different host key.") +          end +          @logger.log("You can pin a different host key using `leap node init NODE`, " + +            "but you must verify the fingerprint of the new host key!") +        end +        exit(1) +      rescue StandardError => exc +        if exc.is_a?(SSHKit::Command::Failed) || exc.is_a?(SSHKit::Runner::ExecuteError) +          if @options[:raise_error] +            raise LeapCli::SSH::ExecuteError, exc.to_s +          elsif @options[:fail_msg] +            @logger.log(@options[:fail_msg], host: @host.hostname, :color => :red) +          else +            @logger.log(:failed, args.join(' '), host: @host.hostname) do +              @logger.log(exc.to_s.strip, wrap: true) +            end +          end +        elsif exc.is_a?(Timeout::Error) || exc.is_a?(Net::SSH::ConnectionTimeout) +          @logger.log(:failed, args.join(' '), host: @host.hostname) do +            @logger.log("Connection timed out") +          end +          if @options[:raise_error] +            raise LeapCli::SSH::TimeoutError, exc.to_s +          end +        else +          raise +        end +        return nil +      end + +      def output +        @output ||= LeapCli::SSH::Formatter.new(@logger, @host) +      end + +    end +  end +end + | 
