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

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

Defined Under Namespace

Modules: Win Classes: RemoteTempdir

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(target) ⇒ Connection

Returns a new instance of Connection.



57
58
59
60
61
62
63
64
# File 'lib/bolt/transport/ssh/connection.rb', line 57

def initialize(target)
  @target = target

  @user = @target.user || Net::SSH::Config.for(target.host)[:user] || Etc.getlogin
  @run_as = nil

  @logger = Logging.logger[@target.host]
end

Instance Attribute Details

#loggerObject (readonly)

Returns the value of attribute logger.



54
55
56
# File 'lib/bolt/transport/ssh/connection.rb', line 54

def logger
  @logger
end

#run_asObject

This method allows the @run_as variable to be used as a per-operation override for the user to run as. When @run_as is unset, the user specified on the target will be used.



151
152
153
# File 'lib/bolt/transport/ssh/connection.rb', line 151

def run_as
  @run_as || target.options['run-as']
end

#targetObject (readonly)

Returns the value of attribute target.



54
55
56
# File 'lib/bolt/transport/ssh/connection.rb', line 54

def target
  @target
end

#userObject (readonly)

Returns the value of attribute user.



54
55
56
# File 'lib/bolt/transport/ssh/connection.rb', line 54

def user
  @user
end

Instance Method Details

#connectObject



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

def connect
  transport_logger = Logging.logger[Net::SSH]
  transport_logger.level = :warn
  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
  options[:verify_host_key] = if target.options['host-key-check']
                                Net::SSH::Verifiers::Secure.new
                              else
                                Net::SSH::Verifiers::Lenient.new
                              end
  options[:timeout] = target.options['connect-timeout'] if target.options['connect-timeout']

  # 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_wide = 'Pageant'.encode('UTF-16LE')
    if Win.FindWindow(pageant_wide, pageant_wide).to_i == 0
      @logger.debug { "Disabling use_agent in net-ssh: pageant process not running" }
      options[:use_agent] = false
    end
  end

  @session = Net::SSH.start(target.host, @user, options)
  @logger.debug { "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.uri}: #{e.message}",
    'HOST_KEY_ERROR'
  )
rescue Net::SSH::ConnectionTimeout
  raise Bolt::Node::ConnectError.new(
    "Timeout after #{target.options['connect-timeout']} seconds connecting to #{target.uri}",
    'CONNECT_ERROR'
  )
rescue StandardError => e
  raise Bolt::Node::ConnectError.new(
    "Failed to connect to #{target.uri}: #{e.message}",
    'CONNECT_ERROR'
  )
end

#disconnectObject



141
142
143
144
145
146
# File 'lib/bolt/transport/ssh/connection.rb', line 141

def disconnect
  if @session && !@session.closed?
    @session.close
    @logger.debug { "Closed session" }
  end
end

#execute(command, sudoable: false, **options) ⇒ Object



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
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
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 197

def execute(command, sudoable: false, **options)
  result_output = Bolt::Node::Output.new
  run_as = options[:run_as] || self.run_as
  escalate = sudoable && run_as && @user != run_as
  use_sudo = escalate && @target.options['run-as-command'].nil?

  command_str = command.is_a?(String) ? command : Shellwords.shelljoin(command)
  if escalate
    if use_sudo
      sudo_flags = ["sudo", "-S", "-u", run_as, "-p", sudo_prompt]
      sudo_flags += ["-E"] if options[:environment]
      sudo_str = Shellwords.shelljoin(sudo_flags)
      command_str = "#{sudo_str} #{command_str}"
    else
      run_as_str = Shellwords.shelljoin(@target.options['run-as-command'] + [run_as])
      command_str = "#{run_as_str} #{command_str}"
    end
  end

  # Including the environment declarations in the shelljoin will escape
  # the = sign, so we have to handle them separately.
  if options[:environment]
    env_decls = options[:environment].map do |env, val|
      "#{env}=#{Shellwords.shellescape(val)}"
    end
    command_str = "#{env_decls.join(' ')} #{command_str}"
  end

  @logger.debug { "Executing: #{command_str}" }

  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|
        unless use_sudo && handled_sudo(channel, data)
          result_output.stdout << data
        end
        @logger.debug { "stdout: #{data.strip}" }
      end

      channel.on_extended_data do |_, _, data|
        unless use_sudo && handled_sudo(channel, data)
          result_output.stderr << data
        end
        @logger.debug { "stderr: #{data.strip}" }
      end

      channel.on_request("exit-status") do |_, data|
        result_output.exit_code = data.read_long
      end

      if options[:stdin]
        channel.send_data(options[:stdin])
        channel.eof!
      end
    end
  end
  session_channel.wait

  if result_output.exit_code == 0
    @logger.debug { "Command returned successfully" }
  else
    @logger.info { "Command failed with exit code #{result_output.exit_code}" }
  end
  result_output
rescue StandardError
  @logger.debug { "Command aborted" }
  raise
end

#handled_sudo(channel, data) ⇒ Object



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

def handled_sudo(channel, data)
  if data.lines.include?(sudo_prompt)
    if target.options['sudo-password']
      channel.send_data "#{target.options['sudo-password']}\n"
      channel.wait
      return true
    else
      # Cancel the sudo prompt to prevent later commands getting stuck
      channel.close
      raise Bolt::Node::EscalateError.new(
        "Sudo password for user #{@user} was not provided for #{target.uri}",
        'NO_PASSWORD'
      )
    end
  elsif data =~ /^#{@user} is not in the sudoers file\./
    @logger.debug { data }
    raise Bolt::Node::EscalateError.new(
      "User #{@user} does not have sudo permission on #{target.uri}",
      'SUDO_DENIED'
    )
  elsif data =~ /^Sorry, try again\./
    @logger.debug { data }
    raise Bolt::Node::EscalateError.new(
      "Sudo password for user #{@user} not recognized on #{target.uri}",
      'BAD_PASSWORD'
    )
  end
  false
end

#make_executable(path) ⇒ Object



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

def make_executable(path)
  result = execute(['chmod', 'u+x', path])
  if result.exit_code != 0
    message = "Could not make file '#{path}' executable: #{result.stderr.string}"
    raise Bolt::Node::FileError.new(message, 'CHMOD_ERROR')
  end
end

#make_tempdirObject



282
283
284
285
286
287
288
289
290
291
292
293
# File 'lib/bolt/transport/ssh/connection.rb', line 282

def make_tempdir
  tmpdir = target.options.fetch('tmpdir', '/tmp')
  tmppath = "#{tmpdir}/#{SecureRandom.uuid}"
  command = ['mkdir', '-m', 700, tmppath]

  result = execute(command)
  if result.exit_code != 0
    raise Bolt::Node::FileError.new("Could not make tempdir: #{result.stderr.string}", 'TEMPDIR_ERROR')
  end
  path = tmppath || result.stdout.string.chomp
  RemoteTempdir.new(self, path)
end

#running_as(user) ⇒ Object

Run as the specified user for the duration of the block.



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

def running_as(user)
  @run_as = user
  yield
ensure
  @run_as = nil
end

#sudo_promptObject



163
164
165
# File 'lib/bolt/transport/ssh/connection.rb', line 163

def sudo_prompt
  '[sudo] Bolt needs to run as another user, password: '
end

#with_remote_tempdirObject

A helper to create and delete a tempdir on the remote system. Yields the directory name.



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

def with_remote_tempdir
  dir = make_tempdir
  yield dir
ensure
  dir&.delete
end

#write_remote_executable(dir, file, filename = nil) ⇒ Object



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

def write_remote_executable(dir, file, filename = nil)
  filename ||= File.basename(file)
  remote_path = "#{dir}/#{filename}"
  write_remote_file(file, remote_path)
  make_executable(remote_path)
  remote_path
end

#write_remote_file(source, destination) ⇒ Object



276
277
278
279
280
# File 'lib/bolt/transport/ssh/connection.rb', line 276

def write_remote_file(source, destination)
  @session.scp.upload!(source, destination)
rescue StandardError => e
  raise Bolt::Node::FileError.new(e.message, 'WRITE_ERROR')
end