Class: Bsdkrun::Client
- Inherits:
-
Object
- Object
- Bsdkrun::Client
- Defined in:
- lib/bsdkrun/client.rb
Overview
A client that talks to a remote bsdkrund daemon's GraphQL API directly
— HTTP for queries/mutations, a hand-rolled graphql-transport-ws socket
for subscriptions — instead of shelling out to a local bsdkrun binary
the way Sandbox does.
The wire contract (URL/header shape, error mapping, subscription
protocol, field names) is locked to match the other bsdkrun SDKs
(TypeScript/Python/Elixir/Gleam) and the web frontend's
web/src/lib/graphql.ts byte-for-byte — see that file for the reference
implementation this one mirrors.
Constant Summary collapse
- URL_ENV =
"BSDKRUN_URL"- TOKEN_ENV =
"BSDKRUN_TOKEN"- MACHINE_FIELDS =
The
Machinefield selection shared bymachines/machine— mirrors +web/src/lib/api.ts+'sMACHINE_FIELDSfragment exactly. <<~GQL.freeze id name image kind command status running exitCode pid detached cpus mem volume stateDir createdAt finishedAt network netIp origin ports { bind host guest } GQL
- SNAPSHOT_FIELDS =
The
Snapshotselection, likewise shared by every snapshot document. <<~GQL.freeze id name machineId machineName kind image path parent description cpus mem size createdAt ports { bind host guest } GQL
- AI_AGENT_FIELDS =
---- ai agents -------------------------------------------------------------
A sandbox is a machine, so its terminal is the ordinary #shell with the argv #ai_shell_command returns.
"id label flavor description installed running"- AI_SESSION_FIELDS =
"id name agent running workspace createdAt"- DOCKER_STATUS_FIELDS =
---- docker --------------------------------------------------------------
bsdkrun runs one
docker:dindmicroVM and serves its API on a host unix socket, so these drive the same engine the host'sdockerCLI does. <<~GQL.freeze running machineId machineRunning socket socketReady apiPort version containers images mounts disk diskSize GQL
- DOCKER_CONTAINER_FIELDS =
"id name image command state status ports created"
Instance Attribute Summary collapse
-
#url ⇒ String
readonly
The GraphQL endpoint URL (normalized).
Class Method Summary collapse
-
.from_env ⇒ Client
Build a client from
BSDKRUN_URL/BSDKRUN_TOKEN. -
.normalize_url(input) ⇒ String
Normalize a user-supplied URL into a full GraphQL endpoint: trim, add
http://if no scheme was given, strip trailing slashes, append/graphqlunless the path already ends with it. -
.ws_url(http_url) ⇒ String
Derive the websocket endpoint from the HTTP one:
http://->ws://,https://->wss://, trailing slashes on the path stripped,/wsappended.
Instance Method Summary collapse
-
#ai_agents ⇒ Array<AiAgent>
The coding agents, and whether each one's sandbox image is built.
-
#ai_remove(agent, keep_home: false) ⇒ CommandResult
Remove an agent's sandboxes, and unless
keep_homeits saved login too. -
#ai_sessions ⇒ Array<AiSession>
Agent sandboxes, newest first.
-
#ai_shell_command(agent, machine_id) ⇒ Array<String>
The argv that starts the agent's TUI — pass it to #shell.
-
#ai_start(agent, cpus: nil, mem: nil, workspace: nil, new: false) ⇒ String
Start (or reuse) a sandbox; returns its machine id.
-
#ai_stop(agent) ⇒ CommandResult
Stop an agent's sandboxes.
-
#branch(snapshot, name: nil, cpus: nil, mem: nil, ports: [], no_ports: false) ⇒ String
Boot a NEW machine from a snapshot — or from a machine, which is snapshotted first — and return the new machine's id.
-
#commit(id, name, description: "") ⇒ CommandResult
Snapshot a machine into a named flavor, like
docker commit. -
#docker_container(action, ids) ⇒ CommandResult
start | stop | restart | kill | pause | unpause | rm.
-
#docker_containers(all: true) ⇒ Array<DockerContainer>
Containers in the engine.
-
#docker_logs(id, tail: 200) ⇒ String
One container's logs (stdout+stderr, most recent
taillines). -
#docker_start(cpus: nil, mem: nil, mounts: [], no_home: false, publish_bind: nil, disk_size: nil) ⇒ DockerStatus
Start (or resume) the engine, returning its status once it answers.
-
#docker_status ⇒ DockerStatus
Is the Docker engine up, and where is its socket?.
-
#docker_stop ⇒ CommandResult
Stop the engine.
-
#exec(id, command, env: nil) ⇒ ExecResult
Run a command to completion and collect its output.
-
#follow_logs(id, follow: true, boot: false) {|data| ... } ⇒ Proc
Stream a machine's console log live.
-
#get(id) ⇒ SandboxInfo?
Nil if no such machine exists.
-
#initialize(url:, token:) ⇒ Client
constructor
A new instance of Client.
- #list(all: false) ⇒ Array<SandboxInfo>
-
#logs(id, boot: false) ⇒ String
One-shot console log fetch.
- #remove(ids, force: false) ⇒ CommandResult
-
#remove_snapshots(names) ⇒ CommandResult
Delete snapshots and their data.
-
#request(query, variables = {}) ⇒ Hash
Run an arbitrary query or mutation.
-
#restore(id, snapshot, force: true, backup: true) ⇒ CommandResult
Put a machine's disk state back to one of its snapshots.
-
#rollback(id, force: true, backup: true) ⇒ CommandResult
Restore a machine to its most recent snapshot.
-
#run_bsd(opts = {}, **kwargs) ⇒ String
The new machine's id.
-
#run_flavor(opts = {}, **kwargs) ⇒ String
The new machine's id.
-
#run_linux(opts = {}, **kwargs) ⇒ String
The new machine's id.
-
#run_nanos(opts = {}, **kwargs) ⇒ String
No agent (no exec/shell), but it does have a root disk, so
persist:is the one disk option it takes. -
#run_osv(opts = {}, **kwargs) ⇒ String
Like Nanos, no agent, but it does have a root filesystem, so the disk options apply —
disk:in particular, how an x86_64 guest gets a filesystem (its loader ELF is kernel only). -
#run_solo5(opts = {}, **kwargs) ⇒ String
Solo5 (MirageOS): runs under the
solo5-hvttender rather than libkrun. -
#run_unikraft(opts = {}, **kwargs) ⇒ String
A unikernel has no disk and no agent, so no volume/persist/repo/command fields —
mounts:(virtio-fs shares) is the exception, needing neither. -
#shell(id, command: nil, env: nil, rows: 24, cols: 80) ⇒ ShellSession
Open a live interactive session.
-
#snapshot(id, name: nil, description: "") ⇒ SnapshotInfo
Capture a machine's disk state.
-
#snapshots(machine: nil) ⇒ Array<SnapshotInfo>
List snapshots, newest first.
- #start(id) ⇒ CommandResult
- #stop(id) ⇒ CommandResult
-
#subscribe(query, variables = {}, on_next:, on_error: nil, on_complete: nil) ⇒ Proc
Start a subscription over the shared websocket (opened lazily on first use).
- #update(id, cpus: nil, mem: nil) ⇒ CommandResult
Constructor Details
#initialize(url:, token:) ⇒ Client
Returns a new instance of Client.
54 55 56 57 58 59 |
# File 'lib/bsdkrun/client.rb', line 54 def initialize(url:, token:) @url = self.class.normalize_url(url) @token = token.to_s @ws_mutex = Mutex.new @ws = nil end |
Instance Attribute Details
#url ⇒ String (readonly)
Returns the GraphQL endpoint URL (normalized).
49 50 51 |
# File 'lib/bsdkrun/client.rb', line 49 def url @url end |
Class Method Details
.from_env ⇒ Client
Build a client from BSDKRUN_URL / BSDKRUN_TOKEN.
A host set without a token is an error, not a silent fallback —
mirrors +daemon/src/client.rs+'s RemoteConfig::from_env (which uses
+BSDKRUN_HOST+/+BSDKRUN_TOKEN+ for the gRPC client; these are
GraphQL-specific env vars with a different URL shape, not aliases).
70 71 72 73 74 75 76 77 78 79 80 |
# File 'lib/bsdkrun/client.rb', line 70 def self.from_env url = ENV[URL_ENV] raise Error, "#{URL_ENV} is not set; nothing to connect to" if url.nil? || url.strip.empty? token = ENV[TOKEN_ENV] if token.nil? || token.strip.empty? raise Error, "#{URL_ENV} is set but #{TOKEN_ENV} is not" end new(url: url, token: token) end |
.normalize_url(input) ⇒ String
Normalize a user-supplied URL into a full GraphQL endpoint: trim, add
http:// if no scheme was given, strip trailing slashes, append
/graphql unless the path already ends with it. Mirrors
+web/src/lib/connection.ts+'s normalizeUrl exactly.
89 90 91 92 93 94 95 96 97 |
# File 'lib/bsdkrun/client.rb', line 89 def self.normalize_url(input) s = input.to_s.strip return s if s.empty? s = "http://#{s}" unless s.match?(%r{\Ahttps?://}i) s = s.sub(%r{/+\z}, "") s = "#{s}/graphql" unless s.match?(%r{/graphql\z}i) s end |
.ws_url(http_url) ⇒ String
Derive the websocket endpoint from the HTTP one: http:// -> ws://,
https:// -> wss://, trailing slashes on the path stripped, /ws
appended. Mirrors +web/src/lib/graphql.ts+'s wsUrl.
105 106 107 108 109 110 |
# File 'lib/bsdkrun/client.rb', line 105 def self.ws_url(http_url) uri = URI.parse(http_url) uri.scheme = uri.scheme == "https" ? "wss" : "ws" uri.path = "#{uri.path.to_s.sub(%r{/+\z}, "")}/ws" uri.to_s end |
Instance Method Details
#ai_agents ⇒ Array<AiAgent>
The coding agents, and whether each one's sandbox image is built.
261 262 263 264 |
# File 'lib/bsdkrun/client.rb', line 261 def ai_agents data = request("{ aiAgents { #{AI_AGENT_FIELDS} } }") (data["aiAgents"] || []).map { |a| AiAgent.from_graphql(a) } end |
#ai_remove(agent, keep_home: false) ⇒ CommandResult
Remove an agent's sandboxes, and unless keep_home its saved login too.
311 312 313 314 315 316 317 318 |
# File 'lib/bsdkrun/client.rb', line 311 def ai_remove(agent, keep_home: false) run_command_mutation( "aiRemove", "mutation($agent:String!,$keepHome:Boolean!){ " \ "aiRemove(agent:$agent, keepHome:$keepHome){ exitCode stdout stderr } }", { agent: agent, keepHome: keep_home } ) end |
#ai_sessions ⇒ Array<AiSession>
Agent sandboxes, newest first.
268 269 270 271 |
# File 'lib/bsdkrun/client.rb', line 268 def ai_sessions data = request("{ aiSessions { #{AI_SESSION_FIELDS} } }") (data["aiSessions"] || []).map { |s| AiSession.from_graphql(s) } end |
#ai_shell_command(agent, machine_id) ⇒ Array<String>
The argv that starts the agent's TUI — pass it to #shell.
290 291 292 293 294 295 296 297 |
# File 'lib/bsdkrun/client.rb', line 290 def ai_shell_command(agent, machine_id) data = request( "query($agent:String!,$machineId:String!){ " \ "aiShellCommand(agent:$agent, machineId:$machineId) }", { agent: agent, machineId: machine_id } ) Array(data["aiShellCommand"]) end |
#ai_start(agent, cpus: nil, mem: nil, workspace: nil, new: false) ⇒ String
Start (or reuse) a sandbox; returns its machine id.
279 280 281 282 283 284 285 286 |
# File 'lib/bsdkrun/client.rb', line 279 def ai_start(agent, cpus: nil, mem: nil, workspace: nil, new: false) data = request( "mutation($input:AiStartInput!){ aiStart(input:$input) }", { input: { agent: agent, cpus: cpus, mem: mem, workspace: workspace, new: new } } ) data["aiStart"].to_s end |
#ai_stop(agent) ⇒ CommandResult
Stop an agent's sandboxes. Its saved login survives.
301 302 303 304 305 306 307 |
# File 'lib/bsdkrun/client.rb', line 301 def ai_stop(agent) run_command_mutation( "aiStop", "mutation($agent:String!){ aiStop(agent:$agent){ exitCode stdout stderr } }", { agent: agent } ) end |
#branch(snapshot, name: nil, cpus: nil, mem: nil, ports: [], no_ports: false) ⇒ String
Boot a NEW machine from a snapshot — or from a machine, which is snapshotted first — and return the new machine's id.
The state is cloned, never booted in place, so the source is untouched
and one snapshot can be branched any number of times. With no ports,
the snapshot's own forwards are inherited, with any host port that is
already taken swapped for a free one.
505 506 507 508 509 510 511 512 |
# File 'lib/bsdkrun/client.rb', line 505 def branch(snapshot, name: nil, cpus: nil, mem: nil, ports: [], no_ports: false) data = request( "mutation($input:BranchInput!){ branchSnapshot(input:$input) }", { input: { snapshot: snapshot, name: name, cpus: cpus, mem: mem, ports: Array(ports), noPorts: no_ports } } ) data["branchSnapshot"].to_s end |
#commit(id, name, description: "") ⇒ CommandResult
Snapshot a machine into a named flavor, like docker commit.
242 243 244 245 246 247 248 249 |
# File 'lib/bsdkrun/client.rb', line 242 def commit(id, name, description: "") run_command_mutation( "commitMachine", "mutation($id:String!,$name:String!,$description:String!){ " \ "commitMachine(id:$id, name:$name, description:$description){ exitCode stdout stderr } }", { id: id, name: name, description: description } ) end |
#docker_container(action, ids) ⇒ CommandResult
start | stop | restart | kill | pause | unpause | rm.
387 388 389 390 391 392 393 394 |
# File 'lib/bsdkrun/client.rb', line 387 def docker_container(action, ids) run_command_mutation( "dockerContainer", "mutation($action:String!,$ids:[String!]!){ " \ "dockerContainer(action:$action, ids:$ids){ exitCode stdout stderr } }", { action: action, ids: Array(ids) } ) end |
#docker_containers(all: true) ⇒ Array<DockerContainer>
Containers in the engine.
342 343 344 345 346 347 348 |
# File 'lib/bsdkrun/client.rb', line 342 def docker_containers(all: true) data = request( "query($all:Boolean!){ dockerContainers(all:$all){ #{DOCKER_CONTAINER_FIELDS} } }", { all: all } ) (data["dockerContainers"] || []).map { |c| DockerContainer.from_graphql(c) } end |
#docker_logs(id, tail: 200) ⇒ String
One container's logs (stdout+stderr, most recent tail lines).
400 401 402 403 404 405 406 |
# File 'lib/bsdkrun/client.rb', line 400 def docker_logs(id, tail: 200) data = request( "query($id:String!,$tail:Int!){ dockerContainerLogs(id:$id, tail:$tail) }", { id: id, tail: tail } ) data["dockerContainerLogs"].to_s end |
#docker_start(cpus: nil, mem: nil, mounts: [], no_home: false, publish_bind: nil, disk_size: nil) ⇒ DockerStatus
Start (or resume) the engine, returning its status once it answers.
Idempotent: the VM has a fixed name, so this resumes the existing one rather than creating a second.
362 363 364 365 366 367 368 369 370 371 |
# File 'lib/bsdkrun/client.rb', line 362 def docker_start(cpus: nil, mem: nil, mounts: [], no_home: false, publish_bind: nil, disk_size: nil) data = request( "mutation($input:DockerStartInput!){ dockerStart(input:$input){ " \ "#{DOCKER_STATUS_FIELDS} } }", { input: { cpus: cpus, mem: mem, mounts: Array(mounts), noHome: no_home, publishBind: publish_bind, diskSize: disk_size } } ) DockerStatus.from_graphql(data["dockerStart"]) end |
#docker_status ⇒ DockerStatus
Is the Docker engine up, and where is its socket?
334 335 336 337 |
# File 'lib/bsdkrun/client.rb', line 334 def docker_status data = request("{ dockerStatus { #{DOCKER_STATUS_FIELDS} } }") DockerStatus.from_graphql(data["dockerStatus"]) end |
#docker_stop ⇒ CommandResult
Stop the engine. Images and containers stay on its disk.
375 376 377 378 379 380 381 |
# File 'lib/bsdkrun/client.rb', line 375 def docker_stop run_command_mutation( "dockerStop", "mutation{ dockerStop{ exitCode stdout stderr } }", {} ) end |
#exec(id, command, env: nil) ⇒ ExecResult
Run a command to completion and collect its output. Implemented as the
three-operation sequence daemon/README.md documents: openShell
(with a command:, so the session runs it instead of a login shell),
THEN subscribe to shellOutput (so nothing written in between is
lost), THEN wait for an exit code. closeShell runs in an ensure so
it happens whether the wait succeeded, failed, or raised.
702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 |
# File 'lib/bsdkrun/client.rb', line 702 def exec(id, command, env: nil) data = request( "mutation($m:String!,$c:[String!]!,$e:[String!]!,$r:Int!,$k:Int!){ " \ "openShell(machineId:$m, command:$c, env:$e, rows:$r, cols:$k){ id } }", { m: id, c: Array(command), e: env_to_list(env), r: 24, k: 80 } ) session_id = data["openShell"]["id"] output = +"".b exit_code = nil done = Queue.new unsubscribe = subscribe( "subscription($s:String!){ shellOutput(sessionId:$s){ dataBase64 exitCode } }", { s: session_id }, on_next: lambda { |d| payload = d && d["shellOutput"] next unless payload output << Base64.decode64(payload["dataBase64"]) if payload["dataBase64"] unless payload["exitCode"].nil? exit_code = payload["exitCode"] done << :done end }, on_error: ->(e) { done << e }, on_complete: -> { done << :done } ) begin result = done.pop raise result if result.is_a?(Exception) ensure unsubscribe.call begin request("mutation($s:String!){ closeShell(sessionId:$s) }", { s: session_id }) rescue GraphQLError # closeShell is idempotent server-side; a request failure here # (already gone, machine removed, etc.) must not mask the actual # exec result/exception above — same as the other SDKs' Client#exec. nil end end ExecResult.new(exit_code: exit_code, output: output) end |
#follow_logs(id, follow: true, boot: false) {|data| ... } ⇒ Proc
Stream a machine's console log live.
530 531 532 533 534 535 536 537 538 539 540 541 542 |
# File 'lib/bsdkrun/client.rb', line 530 def follow_logs(id, follow: true, boot: false, &on_data) subscribe( "subscription($id:String!,$follow:Boolean!,$boot:Boolean!){ " \ "machineLogs(id:$id, follow:$follow, boot:$boot){ dataBase64 exitCode } }", { id: id, follow: follow, boot: boot }, on_next: lambda { |data| payload = data && data["machineLogs"] next unless payload && payload["dataBase64"] on_data&.call(Base64.decode64(payload["dataBase64"])) } ) end |
#get(id) ⇒ SandboxInfo?
Returns nil if no such machine exists.
188 189 190 191 192 |
# File 'lib/bsdkrun/client.rb', line 188 def get(id) data = request("query($id:String!){ machine(id:$id){ #{MACHINE_FIELDS} } }", { id: id }) m = data["machine"] m && SandboxInfo.from_graphql(m) end |
#list(all: false) ⇒ Array<SandboxInfo>
181 182 183 184 |
# File 'lib/bsdkrun/client.rb', line 181 def list(all: false) data = request("query($all:Boolean!){ machines(all:$all){ #{MACHINE_FIELDS} } }", { all: all }) (data["machines"] || []).map { |m| SandboxInfo.from_graphql(m) } end |
#logs(id, boot: false) ⇒ String
One-shot console log fetch. Use #follow_logs to stream instead.
518 519 520 521 522 |
# File 'lib/bsdkrun/client.rb', line 518 def logs(id, boot: false) data = request("query($id:String!,$boot:Boolean!){ machineLogs(id:$id, boot:$boot) }", { id: id, boot: boot }) data["machineLogs"] end |
#remove(ids, force: false) ⇒ CommandResult
217 218 219 220 221 222 223 |
# File 'lib/bsdkrun/client.rb', line 217 def remove(ids, force: false) run_command_mutation( "removeMachines", "mutation($ids:[String!]!,$force:Boolean!){ removeMachines(ids:$ids, force:$force){ exitCode stdout stderr } }", { ids: Array(ids), force: force } ) end |
#remove_snapshots(names) ⇒ CommandResult
Delete snapshots and their data. Machines branched from them are unaffected.
447 448 449 450 451 452 453 |
# File 'lib/bsdkrun/client.rb', line 447 def remove_snapshots(names) run_command_mutation( "removeSnapshots", "mutation($names:[String!]!){ removeSnapshots(names:$names){ exitCode stdout stderr } }", { names: Array(names) } ) end |
#request(query, variables = {}) ⇒ Hash
Run an arbitrary query or mutation. Every typed method on this class is implemented in terms of this — it exists as a public escape hatch for documents this SDK has no typed wrapper for yet.
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 157 158 159 160 |
# File 'lib/bsdkrun/client.rb', line 125 def request(query, variables = {}) uri = URI.parse(@url) http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = uri.scheme == "https" req = Net::HTTP::Post.new(uri.request_uri.empty? ? "/" : uri.request_uri) req["content-type"] = "application/json" req["authorization"] = "Bearer #{@token}" req.body = JSON.generate({ query: query, variables: variables }) begin res = http.request(req) rescue StandardError => e raise GraphQLError, "cannot reach the bsdkrun daemon at #{@url} — #{e.}" end raise AuthError if res.code.to_i == 401 body = begin JSON.parse(res.body.to_s) rescue JSON::ParserError nil end raise GraphQLError, "the daemon returned a non-JSON response (#{res.code})" if body.nil? errors = body["errors"] if errors.is_a?(Array) && !errors.empty? first = errors.first = first["message"].to_s code = first.is_a?(Hash) ? first.dig("extensions", "code") : nil raise AuthError, if code == "UNAUTHENTICATED" raise GraphQLError.new(, code) end body["data"] end |
#restore(id, snapshot, force: true, backup: true) ⇒ CommandResult
Put a machine's disk state back to one of its snapshots.
force stops the machine first (it holds the very files being
replaced); backup snapshots the state being overwritten, which is a
CoW clone and therefore free. The machine is left stopped.
466 467 468 469 470 471 472 473 474 |
# File 'lib/bsdkrun/client.rb', line 466 def restore(id, snapshot, force: true, backup: true) run_command_mutation( "restoreMachine", "mutation($id:String!,$snapshot:String!,$force:Boolean!,$backup:Boolean!){ " \ "restoreMachine(id:$id, snapshot:$snapshot, force:$force, backup:$backup){ " \ "exitCode stdout stderr } }", { id: id, snapshot: snapshot, force: force, backup: backup } ) end |
#rollback(id, force: true, backup: true) ⇒ CommandResult
Restore a machine to its most recent snapshot.
481 482 483 484 485 486 487 488 |
# File 'lib/bsdkrun/client.rb', line 481 def rollback(id, force: true, backup: true) run_command_mutation( "rollbackMachine", "mutation($id:String!,$force:Boolean!,$backup:Boolean!){ " \ "rollbackMachine(id:$id, force:$force, backup:$backup){ exitCode stdout stderr } }", { id: id, force: force, backup: backup } ) end |
#run_bsd(opts = {}, **kwargs) ⇒ String
Returns the new machine's id.
579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 |
# File 'lib/bsdkrun/client.rb', line 579 def run_bsd(opts = {}, **kwargs) o = merge_opts(opts, kwargs) input = { os: bsd_os_enum(o.fetch(:os)), version: o[:version], cpus: o[:cpus], mem: o[:mem], net: net_input(o[:net]), volume: o[:volume], persist: o[:persist] || false, force: o[:force] || false, firmware: o[:firmware], attachDisk: o[:attach_disk] || [], diskSize: o[:disk_size], repo: o[:repo], command: o[:command] || [] } request("mutation($i:RunBsdInput!){ runBsd(input:$i) }", { i: input })["runBsd"] end |
#run_flavor(opts = {}, **kwargs) ⇒ String
Returns the new machine's id.
676 677 678 679 680 681 682 683 684 685 686 687 |
# File 'lib/bsdkrun/client.rb', line 676 def run_flavor(opts = {}, **kwargs) o = merge_opts(opts, kwargs) input = { name: o.fetch(:name), cpus: o[:cpus], mem: o[:mem], ports: o[:ports] || [], volume: o[:volume], repo: o[:repo] } request("mutation($i:RunFlavorInput!){ runFlavor(input:$i) }", { i: input })["runFlavor"] end |
#run_linux(opts = {}, **kwargs) ⇒ String
Returns the new machine's id.
556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 |
# File 'lib/bsdkrun/client.rb', line 556 def run_linux(opts = {}, **kwargs) o = merge_opts(opts, kwargs) input = { image: o.fetch(:image), cpus: o[:cpus], mem: o[:mem], net: net_input(o[:net]), volume: o[:volume], mounts: o[:mounts] || [], attachDisk: o[:attach_disk] || [], env: o[:env] || [], entrypoint: o[:entrypoint], initramfs: o[:initramfs] || false, kernel: o[:kernel], kernelVersion: o[:kernel_version], console: o[:console], repo: o[:repo], command: o[:command] || [] } request("mutation($i:RunLinuxInput!){ runLinux(input:$i) }", { i: input })["runLinux"] end |
#run_nanos(opts = {}, **kwargs) ⇒ String
No agent (no exec/shell), but it does have a root disk, so persist:
is the one disk option it takes.
602 603 604 605 606 607 608 609 610 611 612 613 614 |
# File 'lib/bsdkrun/client.rb', line 602 def run_nanos(opts = {}, **kwargs) o = merge_opts(opts, kwargs) input = { image: o.fetch(:image), cpus: o[:cpus], mem: o[:mem], net: net_input(o[:net]), kernel: o[:kernel], cmdline: o[:cmdline], persist: o[:persist] || false } request("mutation($i:RunNanosInput!){ runNanos(input:$i) }", { i: input })["runNanos"] end |
#run_osv(opts = {}, **kwargs) ⇒ String
Like Nanos, no agent, but it does have a root filesystem, so the disk
options apply — disk: in particular, how an x86_64 guest gets a
filesystem (its loader ELF is kernel only).
657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 |
# File 'lib/bsdkrun/client.rb', line 657 def run_osv(opts = {}, **kwargs) o = merge_opts(opts, kwargs) input = { image: o.fetch(:image), cpus: o[:cpus], mem: o[:mem], net: net_input(o[:net]), cmdline: o[:cmdline], disk: o[:disk], noDisk: o[:no_disk] || false, attachDisk: o[:attach_disk] || [], gic: o[:gic], persist: o[:persist] || false, volume: o[:volume] } request("mutation($i:RunOsvInput!){ runOsv(input:$i) }", { i: input })["runOsv"] end |
#run_solo5(opts = {}, **kwargs) ⇒ String
Solo5 (MirageOS): runs under the solo5-hvt tender rather than libkrun.
The unikernel declares its own network and block devices in its MFT1
manifest note, so only what the host alone can know is asked for —
block: backing files (+NAME=FILE+) and the unikernel's own args:
(e.g. "--ipv4=10.0.0.2/24"). Like Unikraft, no disk and no agent, so no
volume/persist/repo/command fields.
640 641 642 643 644 645 646 647 648 649 650 651 |
# File 'lib/bsdkrun/client.rb', line 640 def run_solo5(opts = {}, **kwargs) o = merge_opts(opts, kwargs) input = { path: o[:path], cpus: o[:cpus], mem: o[:mem], net: net_input(o[:net]), block: o[:block] || [], args: o[:args] || [] } request("mutation($i:RunSolo5Input!){ runSolo5(input:$i) }", { i: input })["runSolo5"] end |
#run_unikraft(opts = {}, **kwargs) ⇒ String
A unikernel has no disk and no agent, so no volume/persist/repo/command
fields — mounts: (virtio-fs shares) is the exception, needing neither.
619 620 621 622 623 624 625 626 627 628 629 630 631 |
# File 'lib/bsdkrun/client.rb', line 619 def run_unikraft(opts = {}, **kwargs) o = merge_opts(opts, kwargs) input = { path: o[:path], cpus: o[:cpus], mem: o[:mem], net: net_input(o[:net]), cmdline: o[:cmdline], initramfs: o[:initramfs], mounts: o[:mounts] || [] } request("mutation($i:RunUnikraftInput!){ runUnikraft(input:$i) }", { i: input })["runUnikraft"] end |
#shell(id, command: nil, env: nil, rows: 24, cols: 80) ⇒ ShellSession
Open a live interactive session. Unlike #exec, this returns immediately with a handle whose ShellSession#on_output / ShellSession#on_exit callbacks fire as output arrives.
759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 |
# File 'lib/bsdkrun/client.rb', line 759 def shell(id, command: nil, env: nil, rows: 24, cols: 80) data = request( "mutation($m:String!,$c:[String!]!,$e:[String!]!,$r:Int!,$k:Int!){ " \ "openShell(machineId:$m, command:$c, env:$e, rows:$r, cols:$k){ id } }", { m: id, c: command.nil? ? [] : Array(command), e: env_to_list(env), r: rows, k: cols } ) session_id = data["openShell"]["id"] session = ShellSession.new(client: self, id: session_id) unsubscribe = subscribe( "subscription($s:String!){ shellOutput(sessionId:$s){ dataBase64 exitCode } }", { s: session_id }, on_next: lambda { |d| payload = d && d["shellOutput"] next unless payload session.deliver_output(Base64.decode64(payload["dataBase64"])) if payload["dataBase64"] session.deliver_exit(payload["exitCode"]) unless payload["exitCode"].nil? }, on_error: ->(_e) { session.deliver_exit(nil) }, on_complete: -> {} ) session.unsubscribe = unsubscribe session end |
#snapshot(id, name: nil, description: "") ⇒ SnapshotInfo
Capture a machine's disk state.
A BSD guest is powered off first — a mounted UFS cannot be cloned consistently — so the machine is left stopped; #start brings it back.
434 435 436 437 438 439 440 441 |
# File 'lib/bsdkrun/client.rb', line 434 def snapshot(id, name: nil, description: "") data = request( "mutation($id:String!,$name:String,$description:String!){ " \ "snapshotMachine(id:$id, name:$name, description:$description){ #{SNAPSHOT_FIELDS} } }", { id: id, name: name, description: description } ) SnapshotInfo.from_graphql(data["snapshotMachine"]) end |
#snapshots(machine: nil) ⇒ Array<SnapshotInfo>
List snapshots, newest first.
417 418 419 420 421 422 423 |
# File 'lib/bsdkrun/client.rb', line 417 def snapshots(machine: nil) data = request( "query($machine:String){ snapshots(machine:$machine){ #{SNAPSHOT_FIELDS} } }", { machine: machine } ) (data["snapshots"] || []).map { |s| SnapshotInfo.from_graphql(s) } end |
#start(id) ⇒ CommandResult
206 207 208 209 210 211 212 |
# File 'lib/bsdkrun/client.rb', line 206 def start(id) run_command_mutation( "startMachine", "mutation($id:String!){ startMachine(id:$id){ exitCode stdout stderr } }", { id: id } ) end |
#stop(id) ⇒ CommandResult
196 197 198 199 200 201 202 |
# File 'lib/bsdkrun/client.rb', line 196 def stop(id) run_command_mutation( "stopMachine", "mutation($id:String!){ stopMachine(id:$id){ exitCode stdout stderr } }", { id: id } ) end |
#subscribe(query, variables = {}, on_next:, on_error: nil, on_complete: nil) ⇒ Proc
Start a subscription over the shared websocket (opened lazily on first use). See WsClient#subscribe for the exact queueing/ack semantics.
173 174 175 |
# File 'lib/bsdkrun/client.rb', line 173 def subscribe(query, variables = {}, on_next:, on_error: nil, on_complete: nil) ws.subscribe(query, variables, on_next: on_next, on_error: on_error, on_complete: on_complete) end |
#update(id, cpus: nil, mem: nil) ⇒ CommandResult
229 230 231 232 233 234 235 |
# File 'lib/bsdkrun/client.rb', line 229 def update(id, cpus: nil, mem: nil) run_command_mutation( "updateMachine", "mutation($id:String!,$cpus:Int,$mem:Int){ updateMachine(id:$id, cpus:$cpus, mem:$mem){ exitCode stdout stderr } }", { id: id, cpus: cpus, mem: mem } ) end |