Class: Xshellz::Sandbox
- Inherits:
-
Object
- Object
- Xshellz::Sandbox
- Defined in:
- lib/xshellz/sandbox.rb
Overview
A remote sandbox: control plane over HTTPS, data plane over SSH.
Create one with Sandbox.create, attach to an existing one with
Sandbox.connect, or enumerate them with Sandbox.list.
The block form destroys the sandbox when the block exits, unless
detach was called:
Xshellz::Sandbox.create do |sbx|
result = sbx.run("echo hello")
end
Constant Summary collapse
- STATUS_RUNNING =
"running"
Instance Attribute Summary collapse
-
#info ⇒ Object
readonly
The last-known control-plane state (a SandboxInfo).
-
#private_key_openssh ⇒ Object
readonly
OpenSSH serialization of the private key authenticating this sandbox's SSH (persist it to reconnect later via
Sandbox.connect).
Class Method Summary collapse
-
.connect(uuid, private_key:, api_key: nil, api_url: nil, http: nil) ⇒ Object
Attach to an existing sandbox by UUID.
-
.create(name: nil, api_key: nil, api_url: nil, http: nil) ⇒ Object
Spawn a new sandbox and return it once it is RUNNING.
-
.find(client, uuid) ⇒ Object
Resolve one sandbox via the list endpoint (there is no GET show route).
-
.list(api_key: nil, api_url: nil, http: nil) ⇒ Array<SandboxInfo>
List the account's active sandboxes (a bare array on the wire).
Instance Method Summary collapse
-
#close ⇒ Object
Close the SSH connection (keeps the box alive).
-
#detach ⇒ Object
Keep the sandbox alive when the
createblock exits. - #detached? ⇒ Boolean
-
#download(remote_path, local_path) ⇒ Object
Download a file from the sandbox to a local path (SFTP).
-
#initialize(info:, client:, private_key_openssh: nil, transport: nil) ⇒ Sandbox
constructor
A new instance of Sandbox.
- #inspect ⇒ Object (also: #to_s)
-
#kill ⇒ Object
Destroy the sandbox (+DELETE /shells/agent/#uuid+).
- #name ⇒ Object
-
#read_file(path) ⇒ Object
Read and return the contents of
pathinside the sandbox (SFTP). -
#refresh ⇒ Object
Re-fetch this sandbox's state from the control plane.
-
#run(command, cwd: nil, env: nil, timeout: nil, &block) ⇒ CommandResult
Run a shell command in the sandbox and wait for it to finish.
- #ssh_command ⇒ Object
- #ssh_host ⇒ Object
- #ssh_port ⇒ Object
-
#start ⇒ Object
Resume an idle-stopped box (+POST /shells/agent/#uuid/start+).
- #status ⇒ Object
-
#upload(local_path, remote_path) ⇒ Object
Upload a local file into the sandbox (SFTP).
-
#uuid ⇒ Object
------------------------------------------------------------------ # Properties ------------------------------------------------------------------ #.
-
#write_file(path, data) ⇒ Object
Write
datatopathinside the sandbox (SFTP).
Constructor Details
#initialize(info:, client:, private_key_openssh: nil, transport: nil) ⇒ Sandbox
Returns a new instance of Sandbox.
25 26 27 28 29 30 31 32 |
# File 'lib/xshellz/sandbox.rb', line 25 def initialize(info:, client:, private_key_openssh: nil, transport: nil) @info = info @client = client @private_key_openssh = private_key_openssh @transport = transport @detached = false @killed = false end |
Instance Attribute Details
#info ⇒ Object (readonly)
The last-known control-plane state (a SandboxInfo).
19 20 21 |
# File 'lib/xshellz/sandbox.rb', line 19 def info @info end |
#private_key_openssh ⇒ Object (readonly)
OpenSSH serialization of the private key authenticating this sandbox's
SSH (persist it to reconnect later via Sandbox.connect).
23 24 25 |
# File 'lib/xshellz/sandbox.rb', line 23 def private_key_openssh @private_key_openssh end |
Class Method Details
.connect(uuid, private_key:, api_key: nil, api_url: nil, http: nil) ⇒ Object
Attach to an existing sandbox by UUID.
private_key is the OpenSSH-format private key whose public half the
box was created with (the private_key_openssh of the original
sandbox).
79 80 81 82 83 84 |
# File 'lib/xshellz/sandbox.rb', line 79 def self.connect(uuid, private_key:, api_key: nil, api_url: nil, http: nil) Ssh.load_private_key!(private_key) client = Client.new(api_key: api_key, api_url: api_url, http: http) info = find(client, uuid) new(info: info, client: client, private_key_openssh: private_key) end |
.create(name: nil, api_key: nil, api_url: nil, http: nil) ⇒ Object
Spawn a new sandbox and return it once it is RUNNING.
An in-memory ed25519 keypair is generated for the box; the private key never leaves this process and the server never sees it. Spawning is synchronous - the box is reachable when this returns.
With a block, the sandbox is yielded and destroyed when the block
exits (unless detach was called); the block's value is returned.
Raises AuthError (missing/invalid API key, insufficient scope, or an account gate), QuotaError (the plan's concurrent sandbox limit is reached or the plan has no sandbox entitlement), or ApiError (other API failures: throttle 429, host capacity 503, ...).
51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
# File 'lib/xshellz/sandbox.rb', line 51 def self.create(name: nil, api_key: nil, api_url: nil, http: nil) keypair = Keys.generate client = Client.new(api_key: api_key, api_url: api_url, http: http) body = { "ssh_public_key" => keypair.public_key_line } body["name"] = name unless name.nil? payload = client.post("/shells/agent", body) sandbox = new( info: SandboxInfo.from_api(payload), client: client, private_key_openssh: keypair.private_key_openssh ) return sandbox unless block_given? begin yield sandbox ensure sandbox.kill unless sandbox.detached? sandbox.close end end |
.find(client, uuid) ⇒ Object
Resolve one sandbox via the list endpoint (there is no GET show route).
95 96 97 98 99 100 101 102 |
# File 'lib/xshellz/sandbox.rb', line 95 def self.find(client, uuid) client.get("/shells/agent").each do |item| info = SandboxInfo.from_api(item) return info if info.uuid == uuid end raise SandboxNotRunningError, "Sandbox #{uuid} was not found among this account's active sandboxes." end |
.list(api_key: nil, api_url: nil, http: nil) ⇒ Array<SandboxInfo>
List the account's active sandboxes (a bare array on the wire).
89 90 91 92 |
# File 'lib/xshellz/sandbox.rb', line 89 def self.list(api_key: nil, api_url: nil, http: nil) client = Client.new(api_key: api_key, api_url: api_url, http: http) client.get("/shells/agent").map { |item| SandboxInfo.from_api(item) } end |
Instance Method Details
#close ⇒ Object
Close the SSH connection (keeps the box alive).
228 229 230 231 |
# File 'lib/xshellz/sandbox.rb', line 228 def close close_transport nil end |
#detach ⇒ Object
Keep the sandbox alive when the create block exits.
Persist private_key_openssh and uuid to re-attach later with
Sandbox.connect.
217 218 219 220 |
# File 'lib/xshellz/sandbox.rb', line 217 def detach @detached = true self end |
#detached? ⇒ Boolean
132 133 134 |
# File 'lib/xshellz/sandbox.rb', line 132 def detached? @detached end |
#download(remote_path, local_path) ⇒ Object
Download a file from the sandbox to a local path (SFTP).
170 171 172 |
# File 'lib/xshellz/sandbox.rb', line 170 def download(remote_path, local_path) transport.download(remote_path, local_path) end |
#inspect ⇒ Object Also known as: to_s
233 234 235 |
# File 'lib/xshellz/sandbox.rb', line 233 def inspect "#<Xshellz::Sandbox uuid=#{uuid.inspect} status=#{status.inspect} ssh=#{ssh_host}:#{ssh_port}>" end |
#kill ⇒ Object
Destroy the sandbox (+DELETE /shells/agent/#uuid+). Idempotent: a 404 (already gone) is swallowed.
200 201 202 203 204 205 206 207 208 209 210 211 |
# File 'lib/xshellz/sandbox.rb', line 200 def kill close_transport return if @killed begin @client.delete("/shells/agent/#{uuid}") rescue ApiError => e raise unless e.status == 404 end @killed = true nil end |
#name ⇒ Object
112 113 114 |
# File 'lib/xshellz/sandbox.rb', line 112 def name @info.name end |
#read_file(path) ⇒ Object
Read and return the contents of path inside the sandbox (SFTP).
160 161 162 |
# File 'lib/xshellz/sandbox.rb', line 160 def read_file(path) transport.read_file(path) end |
#refresh ⇒ Object
Re-fetch this sandbox's state from the control plane.
223 224 225 |
# File 'lib/xshellz/sandbox.rb', line 223 def refresh @info = self.class.find(@client, uuid) end |
#run(command, cwd: nil, env: nil, timeout: nil, &block) ⇒ CommandResult
Run a shell command in the sandbox and wait for it to finish.
A non-zero exit code does NOT raise - inspect result.exit_code. An
optional block receives (:stdout | :stderr, chunk) as output arrives
(the full output is still returned in the result).
Raises SandboxNotRunningError when the box is not running, and
CommandTimeoutError when timeout seconds elapse before exit.
150 151 152 |
# File 'lib/xshellz/sandbox.rb', line 150 def run(command, cwd: nil, env: nil, timeout: nil, &block) transport.exec(Ssh.build_shell_command(command, cwd: cwd, env: env), timeout: timeout, &block) end |
#ssh_command ⇒ Object
128 129 130 |
# File 'lib/xshellz/sandbox.rb', line 128 def ssh_command @info.ssh_command end |
#ssh_host ⇒ Object
120 121 122 |
# File 'lib/xshellz/sandbox.rb', line 120 def ssh_host @info.ssh_host end |
#ssh_port ⇒ Object
124 125 126 |
# File 'lib/xshellz/sandbox.rb', line 124 def ssh_port @info.ssh_port end |
#start ⇒ Object
Resume an idle-stopped box (+POST /shells/agent/#uuid/start+).
Free-tier boxes idle-stop after ~30 minutes; this brings the same box
(same /home, same authorized key) back to running.
182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 |
# File 'lib/xshellz/sandbox.rb', line 182 def start begin payload = @client.post("/shells/agent/#{uuid}/start") rescue ApiError => e if e.status == 404 raise SandboxNotRunningError, "Sandbox #{uuid} has no stopped box to start - it may " \ "already be running, suspended, or deleted." end raise end @info = SandboxInfo.from_api(payload) close_transport self end |
#status ⇒ Object
116 117 118 |
# File 'lib/xshellz/sandbox.rb', line 116 def status @info.status end |
#upload(local_path, remote_path) ⇒ Object
Upload a local file into the sandbox (SFTP).
165 166 167 |
# File 'lib/xshellz/sandbox.rb', line 165 def upload(local_path, remote_path) transport.upload(local_path, remote_path) end |
#uuid ⇒ Object
------------------------------------------------------------------ # Properties ------------------------------------------------------------------ #
108 109 110 |
# File 'lib/xshellz/sandbox.rb', line 108 def uuid @info.uuid end |
#write_file(path, data) ⇒ Object
Write data to path inside the sandbox (SFTP).
155 156 157 |
# File 'lib/xshellz/sandbox.rb', line 155 def write_file(path, data) transport.write_file(path, data) end |