Class: Bolt::Transport::SSH::Connection

Inherits:
Object
  • Object
show all
Defined in:
lib/bolt/transport/ssh/connection.rb

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(target, transport_logger) ⇒ Connection

Returns a new instance of Connection.


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
# File 'lib/bolt/transport/ssh/connection.rb', line 15

def initialize(target, transport_logger)
  # lazy-load expensive gem code
  require 'net/ssh'
  require 'net/ssh/proxy/jump'

  raise Bolt::ValidationError, "Target #{target.safe_name} does not have a host" unless target.host

  @target = target
  @load_config = target.options['load-config']

  ssh_config = @load_config ? Net::SSH::Config.for(target.host) : {}
  @user = @target.user || ssh_config[:user] || Etc.getlogin
  @strict_host_key_checking = ssh_config[:strict_host_key_checking]

  @logger = Bolt::Logger.logger(@target.safe_name)
  @transport_logger = transport_logger
  @logger.trace("Initializing ssh connection to #{@target.safe_name}")

  if target.options['private-key'].instance_of?(String)
    begin
      Bolt::Util.validate_file('ssh key', target.options['private-key'])
    rescue Bolt::FileError => e
      Bolt::Logger.warn("invalid_ssh_key", e.msg)
    end
  end
end

Instance Attribute Details

#loggerObject (readonly)

Returns the value of attribute logger.


13
14
15
# File 'lib/bolt/transport/ssh/connection.rb', line 13

def logger
  @logger
end

#targetObject (readonly)

Returns the value of attribute target.


13
14
15
# File 'lib/bolt/transport/ssh/connection.rb', line 13

def target
  @target
end

#userObject (readonly)

Returns the value of attribute user.


13
14
15
# File 'lib/bolt/transport/ssh/connection.rb', line 13

def user
  @user
end

Instance Method Details

#connectObject


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
# File 'lib/bolt/transport/ssh/connection.rb', line 42

def connect
  options = {
    logger: @transport_logger,
    non_interactive: true
  }

  if (key = target.options['private-key'])
    if key.instance_of?(String)
      options[:keys] = key
    else
      options[:key_data] = [key['key-data']]
    end
  end

  options[:port] = target.port if target.port
  options[:password] = target.password if target.password
  # Support both net-ssh 4 and 5. We use 5 in packaging, but Beaker pins to 4 so we
  # want the gem to be compatible with version 4.
  options[:verify_host_key] = if target.options['host-key-check'].nil?
                                # Fall back to SSH behavior. This variable will only be set in net-ssh 5.3+.
                                if @strict_host_key_checking.nil? || @strict_host_key_checking
                                  net_ssh_verifier(:always)
                                else
                                  # SSH's behavior with StrictHostKeyChecking=no: adds new keys to known_hosts.
                                  # If known_hosts points to /dev/null, then equivalent to :never where it
                                  # accepts any key beacuse they're all new.
                                  net_ssh_verifier(:accept_new_or_tunnel_local)
                                end
                              elsif target.options['host-key-check']
                                net_ssh_verifier(:always)
                              else
                                net_ssh_verifier(:never)
                              end
  options[:timeout] = target.options['connect-timeout'] if target.options['connect-timeout']

  options[:proxy] = Net::SSH::Proxy::Jump.new(target.options['proxyjump']) if target.options['proxyjump']

  # Override the default supported algorithms for net-ssh. By default, a subset of supported algorithms
  # are enabled in 6.x, while several are deprecated and not enabled by default. The *-algorithms
  # options can be used to specify a list of algorithms to enable in net-ssh. Any algorithms not in the
  # list are disabled, including ones that are normally enabled by default. Support for deprecated
  # algorithms will be removed in 7.x.
  # https://github.com/net-ssh/net-ssh#supported-algorithms
  if target.options['encryption-algorithms']
    options[:encryption] = net_ssh_algorithms(:encryption, target.options['encryption-algorithms'])
  end

  if target.options['host-key-algorithms']
    options[:host_key] = net_ssh_algorithms(:host_key, target.options['host-key-algorithms'])
  end

  if target.options['kex-algorithms']
    options[:kex] = net_ssh_algorithms(:kex, target.options['kex-algorithms'])
  end

  if target.options['mac-algorithms']
    options[:hmac] = net_ssh_algorithms(:hmac, target.options['mac-algorithms'])
  end

  # This option was to address discrepency betwen net-ssh host-key-check and ssh(1)
  # For the net-ssh 5.x series it defaults to true, in 6.x it will default to false, and will be removed in 7.x
  # https://github.com/net-ssh/net-ssh/pull/663#issuecomment-469979931
  options[:check_host_ip] = false if Net::SSH::VALID_OPTIONS.include?(:check_host_ip)

  if @load_config
    # Mirroring:
    # https://github.com/net-ssh/net-ssh/blob/master/lib/net/ssh/authentication/agent.rb#L80
    # https://github.com/net-ssh/net-ssh/blob/master/lib/net/ssh/authentication/pageant.rb#L403
    if defined?(UNIXSocket) && UNIXSocket
      if ENV['SSH_AUTH_SOCK'].to_s.empty?
        @logger.debug { "Disabling use_agent in net-ssh: ssh-agent is not available" }
        options[:use_agent] = false
      end
    elsif Bolt::Util.windows?
      pageant = Net::SSH::Authentication::Pageant::Win.FindWindow("Pageant", "Pageant")
      # If pageant is not running
      if pageant.to_i == 0
        @logger.debug { "Disabling use_agent in net-ssh: pageant process not running" }
        options[:use_agent] = false
      end
    end
  else
    # Disable ssh config and ssh-agent if requested via load_config
    options[:config] = false
    options[:use_agent] = false
  end

  @session = Net::SSH.start(target.host, @user, options)
  validate_ssh_version
  @logger.trace { "Opened session" }
rescue Net::SSH::AuthenticationFailed => e
  raise Bolt::Node::ConnectError.new(
    e.message,
    'AUTH_ERROR'
  )
rescue Net::SSH::HostKeyError => e
  raise Bolt::Node::ConnectError.new(
    "Host key verification failed for #{target.safe_name}: #{e.message}",
    'HOST_KEY_ERROR'
  )
rescue Net::SSH::ConnectionTimeout
  raise Bolt::Node::ConnectError.new(
    "Timeout after #{target.options['connect-timeout']} seconds connecting to #{target.safe_name}",
    'CONNECT_ERROR'
  )
rescue StandardError => e
  raise Bolt::Node::ConnectError.new(
    "Failed to connect to #{target.safe_name}: #{e.message}",
    'CONNECT_ERROR'
  )
end

#disconnectObject


154
155
156
157
158
159
160
161
162
163
# File 'lib/bolt/transport/ssh/connection.rb', line 154

def disconnect
  if @session && !@session.closed?
    begin
      Timeout.timeout(@target.options['disconnect-timeout']) { @session.close }
    rescue Timeout::Error
      @session.shutdown!
    end
    @logger.trace { "Closed session" }
  end
end

#download_file(source, destination, _download) ⇒ Object


243
244
245
246
247
248
249
# File 'lib/bolt/transport/ssh/connection.rb', line 243

def download_file(source, destination, _download)
  # Do not log wrapper script content
  @logger.trace { "Downloading #{source} to #{destination}" }
  @session.scp.download!(source, destination, recursive: true)
rescue StandardError => e
  raise Bolt::Node::FileError.new(e.message, 'WRITE_ERROR')
end

#execute(command_str) ⇒ Object


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
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
# File 'lib/bolt/transport/ssh/connection.rb', line 165

def execute(command_str)
  in_rd, in_wr = IO.pipe
  out_rd, out_wr = IO.pipe
  err_rd, err_wr = IO.pipe
  th = Thread.new do
    exit_code = nil
    session_channel = @session.open_channel do |channel|
      # Request a pseudo tty
      channel.request_pty if target.options['tty']

      channel.exec(command_str) do |_, success|
        unless success
          raise Bolt::Node::ConnectError.new(
            "Could not execute command: #{command_str.inspect}",
            'EXEC_ERROR'
          )
        end

        channel.on_data do |_, data|
          out_wr << data
        end

        channel.on_extended_data do |_, _, data|
          err_wr << data
        end

        channel.on_request("exit-status") do |_, data|
          exit_code = data.read_long
        end
      end
    end
    write_th = Thread.new do
      chunk_size = 4096
      eof = false
      active = true
      readable = false
      while active && !eof
        @session.loop(0.1) do
          active = session_channel.active?
          readable = select([in_rd], [], [], 0)
          # Loop as long as the channel is still live and there's nothing to be written
          active && !readable
        end
        if readable
          if in_rd.eof?
            session_channel.eof!
            eof = true
          else
            to_write = in_rd.readpartial(chunk_size)
            session_channel.send_data(to_write)
          end
        end
      end
      session_channel.wait
    end
    write_th.join
    exit_code
  ensure
    write_th.terminate
    in_rd.close
    out_wr.close
    err_wr.close
  end
  [in_wr, out_rd, err_rd, th]
rescue Errno::EMFILE => e
  msg = "#{e.message}. This might be resolved by increasing your user limit "\
    "with 'ulimit -n 1024'. See https://puppet.com/docs/bolt/latest/bolt_known_issues.html for details."
  raise Bolt::Error.new(msg, 'bolt/too-many-files')
end

#max_command_lengthObject


303
304
305
306
307
# File 'lib/bolt/transport/ssh/connection.rb', line 303

def max_command_length
  if target.options['login-shell'] == 'powershell'
    32000
  end
end

#net_ssh_algorithms(type, algorithms) ⇒ Object

Add all default algorithms if the 'defaults' key is present and filter out any unsupported algorithms.


278
279
280
281
282
283
284
285
286
287
# File 'lib/bolt/transport/ssh/connection.rb', line 278

def net_ssh_algorithms(type, algorithms)
  if algorithms.include?('defaults')
    defaults = Net::SSH::Transport::Algorithms::DEFAULT_ALGORITHMS[type]
    algorithms += defaults
  end

  known = Net::SSH::Transport::Algorithms::ALGORITHMS[type]

  algorithms & known
end

#net_ssh_verifier(verifier) ⇒ Object

This handles renaming Net::SSH verifiers between version 4.x and 5.x of the gem


253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
# File 'lib/bolt/transport/ssh/connection.rb', line 253

def net_ssh_verifier(verifier)
  case verifier
  when :always
    if defined?(Net::SSH::Verifiers::Always)
      Net::SSH::Verifiers::Always.new
    else
      Net::SSH::Verifiers::Secure.new
    end
  when :never
    if defined?(Net::SSH::Verifiers::Never)
      Net::SSH::Verifiers::Never.new
    else
      Net::SSH::Verifiers::Null.new
    end
  when :accept_new_or_tunnel_local
    if defined?(Net::SSH::Verifiers::AcceptNewOrLocalTunnel)
      Net::SSH::Verifiers::AcceptNewOrLocalTunnel.new
    else
      Net::SSH::Verifiers::Lenient.new
    end
  end
end

#reset_cwd?Boolean

This is used by the Bash shell to decide whether to `cd` before executing commands as a run-as user

Returns:

  • (Boolean)

299
300
301
# File 'lib/bolt/transport/ssh/connection.rb', line 299

def reset_cwd?
  true
end

#shellObject


289
290
291
292
293
294
295
# File 'lib/bolt/transport/ssh/connection.rb', line 289

def shell
  @shell ||= if target.options['login-shell'] == 'powershell'
               Bolt::Shell::Powershell.new(target, self)
             else
               Bolt::Shell::Bash.new(target, self)
             end
end

#upload_file(source, destination) ⇒ Object


235
236
237
238
239
240
241
# File 'lib/bolt/transport/ssh/connection.rb', line 235

def upload_file(source, destination)
  # Do not log wrapper script content
  @logger.trace { "Uploading #{source} to #{destination}" } unless source.is_a?(StringIO)
  @session.scp.upload!(source, destination, recursive: true)
rescue StandardError => e
  raise Bolt::Node::FileError.new(e.message, 'WRITE_ERROR')
end

#validate_ssh_versionObject


309
310
311
312
313
314
315
316
317
# File 'lib/bolt/transport/ssh/connection.rb', line 309

def validate_ssh_version
  remote_version = @session.transport.server_version.version
  return unless target.options['login-shell'] && remote_version

  match = remote_version.match(/OpenSSH_for_Windows_(\d+\.\d+)/)
  if match && match[1].to_f < 7.9
    raise "Powershell over SSH requires OpenSSH server >= 7.9, target is running #{match[1]}"
  end
end