Class: Xshellz::Ssh::NetSshTransport

Inherits:
Object
  • Object
show all
Defined in:
lib/xshellz/ssh.rb

Overview

Real SSH transport: exec over a session channel, files over SFTP.

The private key is handed to net-ssh in memory via key_data - it never touches disk. Requires the ed25519 + bcrypt_pbkdf gems (declared as runtime dependencies) for net-ssh to accept ed25519 keys.

Constant Summary collapse

POLL_INTERVAL =
0.02

Instance Method Summary collapse

Constructor Details

#initialize(host:, port:, private_key_openssh:, username: "root", connect_timeout: 30) ⇒ NetSshTransport

Returns a new instance of NetSshTransport.



56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# File 'lib/xshellz/ssh.rb', line 56

def initialize(host:, port:, private_key_openssh:, username: "root", connect_timeout: 30)
  @ssh = Net::SSH.start(
    host,
    username,
    port: port,
    key_data: [private_key_openssh],
    keys: [],
    keys_only: true,
    auth_methods: ["publickey"],
    non_interactive: true,
    # Sandbox host keys are minted at spawn time; nothing to pin against.
    verify_host_key: :never,
    timeout: connect_timeout
  )
  @sftp = nil
end

Instance Method Details

#closeObject



134
135
136
137
# File 'lib/xshellz/ssh.rb', line 134

def close
  @sftp = nil
  @ssh.close unless @ssh.closed?
end

#download(remote_path, local_path) ⇒ Object



129
130
131
132
# File 'lib/xshellz/ssh.rb', line 129

def download(remote_path, local_path)
  sftp.download!(remote_path, local_path)
  nil
end

#exec(command, timeout: nil, &block) ⇒ Object

Execute a command; optional block receives (:stdout | :stderr, chunk) as output arrives. Returns a CommandResult; a non-zero exit code does NOT raise.



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

def exec(command, timeout: nil, &block)
  stdout = +""
  stderr = +""
  exit_code = nil

  channel = @ssh.open_channel do |ch|
    ch.exec(command) do |_, success|
      raise Error, "Failed to start command on the sandbox: #{command.inspect}" unless success

      ch.on_data do |_, data|
        stdout << data
        block&.call(:stdout, data)
      end
      ch.on_extended_data do |_, _type, data|
        stderr << data
        block&.call(:stderr, data)
      end
      ch.on_request("exit-status") do |_, buffer|
        exit_code = buffer.read_long
      end
    end
  end

  deadline = timeout && (monotonic + timeout)
  timed_out = false
  @ssh.loop(POLL_INTERVAL) do
    timed_out = !deadline.nil? && monotonic > deadline
    channel.active? && !timed_out
  end

  if timed_out && channel.active?
    close_channel_quietly(channel)
    raise CommandTimeoutError,
          "Command did not finish within #{timeout} seconds: #{command.inspect}"
  end

  CommandResult.new(stdout: stdout, stderr: stderr, exit_code: exit_code || -1)
end

#read_file(path) ⇒ Object



115
116
117
# File 'lib/xshellz/ssh.rb', line 115

def read_file(path)
  sftp.download!(path)
end

#upload(local_path, remote_path) ⇒ Object



124
125
126
127
# File 'lib/xshellz/ssh.rb', line 124

def upload(local_path, remote_path)
  sftp.upload!(local_path, remote_path)
  nil
end

#write_file(path, data) ⇒ Object



119
120
121
122
# File 'lib/xshellz/ssh.rb', line 119

def write_file(path, data)
  sftp.upload!(StringIO.new(data), path)
  nil
end