Class: Remotus::SshConnection
- Inherits:
-
Object
- Object
- Remotus::SshConnection
- Extended by:
- Forwardable
- Defined in:
- lib/remotus/ssh_connection.rb
Overview
Class representing an SSH connection to a host
Defined Under Namespace
Classes: GatewayConnection
Constant Summary collapse
- REMOTE_PORT =
Standard SSH remote port
22
- KEEPALIVE_INTERVAL =
Standard SSH keepalive interval
300
- DEFAULT_RETRIES =
Number of default retries
8
- BASE_CONNECT_OPTIONS =
Base options for new SSH connections
{ non_interactive: true, keepalive: true, keepalive_interval: KEEPALIVE_INTERVAL }.freeze
Instance Attribute Summary collapse
-
#host ⇒ String
readonly
Host hostname.
-
#host_pool ⇒ Remotus::HostPool
readonly
Host_pool associated host pool.
-
#port ⇒ Integer
readonly
Remote port.
Instance Method Summary collapse
-
#base_connection ⇒ Net::SSH::Connection::Session
Retrieves/creates the base SSH connection for the host If the base connection already exists, the existing connection will be retrieved.
-
#close ⇒ Object
Closes the current SSH connection if it is active.
-
#connection ⇒ Net::SSH::Connection::Session
Retrieves/creates the SSH connection for the host If the connection already exists, the existing connection will be retrieved.
-
#download(remote_path, local_path = nil, options = {}) ⇒ String
Downloads a file from the remote host to the local host.
-
#file_exist?(remote_path, **options) ⇒ Boolean
Checks if a remote file or directory exists.
-
#initialize(host, port = REMOTE_PORT, host_pool: nil) ⇒ SshConnection
constructor
Creates an SshConnection.
-
#port_open? ⇒ Boolean
Whether the remote host's SSH port is available.
-
#run(command, *args, **options) ⇒ Remotus::Result
Runs a command on the host.
-
#run_script(local_path, remote_path, *args, **options) ⇒ Remotus::Result
Uploads a script and runs it on the host.
-
#type ⇒ Symbol
Connection type.
-
#upload(local_path, remote_path, options = {}) ⇒ String
Uploads a file from the local host to the remote host.
Constructor Details
#initialize(host, port = REMOTE_PORT, host_pool: nil) ⇒ SshConnection
Creates an SshConnection
90 91 92 93 94 95 |
# File 'lib/remotus/ssh_connection.rb', line 90 def initialize(host, port = REMOTE_PORT, host_pool: nil) Remotus.logger.debug { "Creating SshConnection #{object_id} for #{host}" } @host = host @port = port @host_pool = host_pool end |
Instance Attribute Details
#host ⇒ String (readonly)
Returns host hostname.
32 33 34 |
# File 'lib/remotus/ssh_connection.rb', line 32 def host @host end |
#host_pool ⇒ Remotus::HostPool (readonly)
Returns host_pool associated host pool.
35 36 37 |
# File 'lib/remotus/ssh_connection.rb', line 35 def host_pool @host_pool end |
#port ⇒ Integer (readonly)
Returns Remote port.
29 30 31 |
# File 'lib/remotus/ssh_connection.rb', line 29 def port @port end |
Instance Method Details
#base_connection ⇒ Net::SSH::Connection::Session
Retrieves/creates the base SSH connection for the host If the base connection already exists, the existing connection will be retrieved
The SSH connection will be the same whether it is retrieved via base_connection or connection.
114 115 116 |
# File 'lib/remotus/ssh_connection.rb', line 114 def base_connection connection end |
#close ⇒ Object
Closes the current SSH connection if it is active
161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 |
# File 'lib/remotus/ssh_connection.rb', line 161 def close Remotus.logger.debug { "Closing SSH connection." } begin @connection&.close rescue StandardError => e Remotus.logger.warn { "Failed to close existing SSH connection with error #{e}" } end begin @gateway&.connection&.shutdown! if via_gateway? rescue StandardError => e Remotus.logger.warn { "Failed to close existing SSH gateway connection with error #{e}" } end Remotus.logger.debug { "Setting @gateway and @connection to nil." } @gateway = nil @connection = nil end |
#connection ⇒ Net::SSH::Connection::Session
Retrieves/creates the SSH connection for the host If the connection already exists, the existing connection will be retrieved
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 |
# File 'lib/remotus/ssh_connection.rb', line 124 def connection return @connection unless restart_connection? # Close any active connections close target_cred = Remotus::Auth.credential(self) Remotus.logger.debug { "Initializing SSH connection to #{target_cred.user}@#{@host}:#{@port}" } = BASE_CONNECT_OPTIONS.dup [:password] = target_cred.password if target_cred.password [:keys] = [target_cred.private_key] if target_cred.private_key [:key_data] = [target_cred.private_key_data] if target_cred.private_key_data [:port] = @port || REMOTE_PORT if via_gateway? @gateway = GatewayConnection.new(@host_pool[:gateway_host], @host_pool[:gateway_port], @host_pool[:gateway_metadata]) gateway_cred = Remotus::Auth.credential(@gateway) = BASE_CONNECT_OPTIONS.dup [:port] = @gateway.port || REMOTE_PORT [:password] = gateway_cred.password if gateway_cred.password [:keys] = [gateway_cred.private_key] if gateway_cred.private_key [:key_data] = [gateway_cred.private_key_data] if gateway_cred.private_key_data Remotus.logger.debug { "Initializing SSH gateway connection to #{gateway_cred.user}@#{@gateway.host}:#{[:port]}" } @gateway.connection = Net::SSH::Gateway.new(remote_gateway_host, gateway_cred.user, **) @connection = @gateway.connection.ssh(remote_host, target_cred.user, **) else @connection = Net::SSH.start(remote_host, target_cred.user, **) end end |
#download(remote_path, local_path = nil, options = {}) ⇒ String
Downloads a file from the remote host to the local host
384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 |
# File 'lib/remotus/ssh_connection.rb', line 384 def download(remote_path, local_path = nil, = {}) # Support short calling syntax (download("remote_path", option1: 123, option2: 234)) if local_path.is_a?(Hash) = local_path local_path = nil end # Sudo prep if [:sudo] # Must first copy the file to an accessible directory for the login user to download it user_remote_path = sudo_remote_file_path(remote_path) Remotus.logger.debug { "Sudo enabled, copying file from #{@host}:#{remote_path} to #{@host}:#{user_remote_path}" } run("/bin/cp -f '#{remote_path}' '#{user_remote_path}' && chown #{Remotus::Auth.credential(self).user} '#{user_remote_path}'", sudo: true).error! remote_path = user_remote_path end Remotus.logger.debug { "Downloading file from #{@host}:#{remote_path}" } result = nil with_retries("Download #{remote_path} to #{local_path}", [:retries] || DEFAULT_RETRIES) do result = connection.scp.download!(remote_path, local_path, ) end # Return the file content if that is desired local_path.nil? ? result : local_path ensure # Sudo cleanup if [:sudo] Remotus.logger.debug { "Sudo enabled, removing temporary file from #{@host}:#{user_remote_path}" } run("/bin/rm -f '#{user_remote_path}'", sudo: true).error! end end |
#file_exist?(remote_path, **options) ⇒ Boolean
Checks if a remote file or directory exists
429 430 431 432 |
# File 'lib/remotus/ssh_connection.rb', line 429 def file_exist?(remote_path, **) Remotus.logger.debug { "Checking if file #{remote_path} exists on #{@host}" } run("test -f '#{remote_path}' || test -d '#{remote_path}'", **).success? end |
#port_open? ⇒ Boolean
Whether the remote host's SSH port is available
187 188 189 |
# File 'lib/remotus/ssh_connection.rb', line 187 def port_open? Remotus.port_open?(remote_host, @port) end |
#run(command, *args, **options) ⇒ Remotus::Result
Runs a command on the host
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 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 |
# File 'lib/remotus/ssh_connection.rb', line 212 def run(command, *args, **) command = "#{command}#{args.empty? ? "" : " "}#{args.join(" ")}" input = [:input] || +"" stdout = +"" stderr = +"" output = +"" exit_code = nil retries ||= [:retries] || DEFAULT_RETRIES accepted_exit_codes = [:accepted_exit_codes] || [0] ssh_command = command # Refer to the command by object_id throughout the log to avoid logging sensitive data Remotus.logger.debug { "Preparing to run command #{command.object_id} on #{@host}" } with_retries(command, retries) do # Handle sudo if [:sudo] Remotus.logger.debug { "Sudo is enabled for command #{command.object_id}" } ssh_command = "sudo -p '' -S sh -c '#{command.gsub("'", "'\"'\"'")}'" input = "#{Remotus::Auth.credential(self).password}\n#{input}" # If password was nil, raise an exception raise Remotus::MissingSudoPassword, "#{host} credential does not have a password specified" if input.start_with?("\n") end # Allocate a terminal if specified pty = [:pty] || false skip_first_output = pty && [:sudo] # Open an SSH channel to the host channel_handle = connection.open_channel do |channel| # Execute the command if pty Remotus.logger.debug { "Requesting pty for command #{command.object_id}" } channel.request_pty do |ch, success| raise Remotus::PtyError, "could not obtain pty" unless success ch.exec(ssh_command) end else Remotus.logger.debug { "Executing command #{command.object_id}" } channel.exec(ssh_command) end # Provide input unless input.empty? Remotus.logger.debug { "Sending input for command #{command.object_id}" } channel.send_data input channel.eof! end # Process stdout channel.on_data do |ch, data| # Skip the first iteration if sudo and pty is enabled to avoid outputting the sudo password if skip_first_output skip_first_output = false next end stdout << data output << data [:on_stdout].call(ch, data) if [:on_stdout].respond_to?(:call) [:on_output].call(ch, data) if [:on_output].respond_to?(:call) end # Process stderr channel.on_extended_data do |ch, _, data| stderr << data output << data [:on_stderr].call(ch, data) if [:on_stderr].respond_to?(:call) [:on_output].call(ch, data) if [:on_output].respond_to?(:call) end # Process exit status/code channel.on_request("exit-status") do |_, data| exit_code = data.read_long end end # Block until the command has completed execution channel_handle.wait Remotus.logger.debug { "Generating result for command #{command.object_id}" } result = Remotus::Result.new(command, stdout, stderr, output, exit_code) # If we are using sudo and experience an authentication failure, raise an exception if [:sudo] && result.error? && !result.stderr.empty? && result.stderr.match?(/^sudo: \d+ incorrect password attempts?$/) raise Remotus::AuthenticationError, "Could not authenticate to sudo as #{Remotus::Auth.credential(self).user}" end # Perform success, error, and completion callbacks [:on_success].call(result) if [:on_success].respond_to?(:call) && result.success?(accepted_exit_codes) [:on_error].call(result) if [:on_error].respond_to?(:call) && result.error?(accepted_exit_codes) [:on_complete].call(result) if [:on_complete].respond_to?(:call) result end end |
#run_script(local_path, remote_path, *args, **options) ⇒ Remotus::Result
Uploads a script and runs it on the host
333 334 335 336 337 338 |
# File 'lib/remotus/ssh_connection.rb', line 333 def run_script(local_path, remote_path, *args, **) upload(local_path, remote_path, **) Remotus.logger.debug { "Running script #{remote_path} on #{@host}" } run("chmod +x #{remote_path}", **) run(remote_path, *args, **) end |
#type ⇒ Symbol
Connection type
102 103 104 |
# File 'lib/remotus/ssh_connection.rb', line 102 def type :ssh end |
#upload(local_path, remote_path, options = {}) ⇒ String
Uploads a file from the local host to the remote host
354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 |
# File 'lib/remotus/ssh_connection.rb', line 354 def upload(local_path, remote_path, = {}) Remotus.logger.debug { "Uploading file #{local_path} to #{@host}:#{remote_path}" } if [:sudo] sudo_upload(local_path, remote_path, ) else = (remote_path, [:owner], [:group], [:mode]) with_retries("Upload #{local_path} to #{remote_path}", [:retries] || DEFAULT_RETRIES) do connection.scp.upload!(local_path, remote_path, ) end run().error! unless .empty? end remote_path end |