File: netssh.rb

package info (click to toggle)
ruby-sshkit 1.21.2-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 700 kB
  • sloc: ruby: 3,522; makefile: 2
file content (189 lines) | stat: -rw-r--r-- 5,947 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
require 'English'
require 'strscan'
require 'mutex_m'
require 'net/ssh'
require 'net/scp'

module Net
  module SSH
    class Config
      class << self
        remove_method :default_files

        def default_files
          @@default_files + [File.join(Dir.pwd, '.ssh/config')]
        end
      end
    end
  end
end

module SSHKit

  module Backend

    class Netssh < Abstract
      class Configuration
        attr_accessor :connection_timeout, :pty
        attr_writer :ssh_options

        def ssh_options
          default_options.merge(@ssh_options ||= {})
        end

        private

        if Net::SSH::VALID_OPTIONS.include?(:known_hosts)
          def default_options
            @default_options ||= {known_hosts: SSHKit::Backend::Netssh::KnownHosts.new}
            assign_defaults
          end
        else
          def default_options
            @default_options ||= {}
            assign_defaults
          end
        end

        # Set default options early for ConnectionPool cache key
        def assign_defaults
          if Net::SSH.respond_to?(:assign_defaults)
            Net::SSH.assign_defaults(@default_options)
          else
            # net-ssh < 4.0.0 doesn't have assign_defaults
            unless @default_options.key?(:logger)
              require 'logger'
              @default_options[:logger] = ::Logger.new(STDERR)
              @default_options[:logger].level = ::Logger::FATAL
            end
          end
          @default_options
        end
      end

      def upload!(local, remote, options = {})
        summarizer = transfer_summarizer('Uploading', options)
        remote = File.join(pwd_path, remote) unless remote.to_s.start_with?("/") || pwd_path.nil?
        with_ssh do |ssh|
          ssh.scp.upload!(local, remote, options, &summarizer)
        end
      end

      def download!(remote, local=nil, options = {})
        summarizer = transfer_summarizer('Downloading', options)
        remote = File.join(pwd_path, remote) unless remote.to_s.start_with?("/") || pwd_path.nil?
        with_ssh do |ssh|
          ssh.scp.download!(remote, local, options, &summarizer)
        end
      end

      # Note that this pool must be explicitly closed before Ruby exits to
      # ensure the underlying IO objects are properly cleaned up. We register an
      # at_exit handler to do this automatically, as long as Ruby is exiting
      # cleanly (i.e. without an exception).
      @pool = SSHKit::Backend::ConnectionPool.new
      at_exit { @pool.close_connections if @pool && !$ERROR_INFO }

      class << self
        attr_accessor :pool

        def configure
          yield config
        end

        def config
          @config ||= Configuration.new
        end
      end

      private

      def transfer_summarizer(action, options = {})
        log_percent = options[:log_percent] || 10
        log_percent = 100 if log_percent <= 0
        last_name = nil
        last_percentage = nil
        proc do |_ch, name, transferred, total|
          percentage = (transferred.to_f * 100 / total.to_f)
          unless percentage.nan?
            message = "#{action} #{name} #{percentage.round(2)}%"
            percentage_r = (percentage / log_percent).truncate * log_percent
            if percentage_r > 0 && (last_name != name || last_percentage != percentage_r)
              verbosity = (options[:verbosity] || :INFO).downcase # TODO: ideally reuse command.rb logic
              public_send verbosity, message
              last_name = name
              last_percentage = percentage_r
            else
              debug message
            end
          else
            warn "Error calculating percentage #{transferred}/#{total}, " <<
              "is #{name} empty?"
          end
        end
      end

      def execute_command(cmd)
        output.log_command_start(cmd.with_redaction)
        cmd.started = true
        exit_status = nil
        with_ssh do |ssh|
          ssh.open_channel do |chan|
            chan.request_pty if Netssh.config.pty
            chan.exec cmd.to_command do |_ch, _success|
              chan.on_data do |ch, data|
                cmd.on_stdout(ch, data)
                output.log_command_data(cmd, :stdout, data)
              end
              chan.on_extended_data do |ch, _type, data|
                cmd.on_stderr(ch, data)
                output.log_command_data(cmd, :stderr, data)
              end
              chan.on_request("exit-status") do |_ch, data|
                exit_status = data.read_long
              end
              #chan.on_request("exit-signal") do |ch, data|
              #  # TODO: This gets called if the program is killed by a signal
              #  # might also be a worthwhile thing to report
              #  exit_signal = data.read_string.to_i
              #  warn ">>> " + exit_signal.inspect
              #  output.log_command_killed(cmd, exit_signal)
              #end
              chan.on_open_failed do |_ch|
                # TODO: What do do here?
                # I think we should raise something
              end
              chan.on_process do |_ch|
                # TODO: I don't know if this is useful
              end
              chan.on_eof do |_ch|
                # TODO: chan sends EOF before the exit status has been
                # writtend
              end
            end
            chan.wait
          end
          ssh.loop
        end
        # Set exit_status and log the result upon completion
        if exit_status
          cmd.exit_status = exit_status
          output.log_command_exit(cmd)
        end
      end

      def with_ssh(&block)
        host.ssh_options = self.class.config.ssh_options.merge(host.ssh_options || {})
        self.class.pool.with(
          Net::SSH.method(:start),
          String(host.hostname),
          host.username,
          host.netssh_options,
          &block
        )
      end

    end
  end

end