Class: Bsdkrun::Sandbox

Inherits:
Object
  • Object
show all
Defined in:
lib/bsdkrun/sandbox.rb

Overview

A handle to a running (or stopped) bsdkrun microVM.

Create one with Sandbox.create, reconnect with Sandbox.get, or enumerate with Sandbox.list.

Examples:

box = Bsdkrun::Sandbox.create(os: "linux", image: "alpine")
box.exec(["uname", "-a"]).text
box.stop

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(id, ssh_port: nil) ⇒ Sandbox

Returns a new instance of Sandbox.

Parameters:

  • id (String)
  • ssh_port (Integer, nil) (defaults to: nil)


30
31
32
33
# File 'lib/bsdkrun/sandbox.rb', line 30

def initialize(id, ssh_port: nil)
  @id = id
  @ssh_port = ssh_port
end

Instance Attribute Details

#idString (readonly)

The machine's Docker-style short id.

Returns:

  • (String)


22
23
24
# File 'lib/bsdkrun/sandbox.rb', line 22

def id
  @id
end

#ssh_portInteger? (readonly)

Host port forwarded to the guest's SSH, if the boot banner reported one.

Returns:

  • (Integer, nil)


26
27
28
# File 'lib/bsdkrun/sandbox.rb', line 26

def ssh_port
  @ssh_port
end

Class Method Details

.create(opts = {}, **kwargs) ⇒ Sandbox

Boot a new microVM and return a handle to it.

Accepts create options as a keyword list or a Hash; discriminated on :os ("linux", "freebsd", "netbsd", "firmware", "kernel").

Parameters:

  • opts (Hash) (defaults to: {})

Returns:

Raises:



44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# File 'lib/bsdkrun/sandbox.rb', line 44

def create(opts = {}, **kwargs)
  opts = normalize(opts.merge(kwargs))
  args = Args.build_create_args(opts)
  res = Process.run(args, log_level: opts.fetch(:log_level, 1))
  if res.exit_code != 0
    raise CommandFailed.new(
      exit_code: res.exit_code, stdout: res.stdout, stderr: res.stderr,
      command: "bsdkrun create"
    )
  end
  # Detached runs print just the machine id on stdout.
  id = res.stdout.split("\n").map(&:strip).select { |l| l.match?(ID_RE) }.last
  unless id
    raise CommandFailed.new(
      exit_code: res.exit_code, stdout: res.stdout, stderr: res.stderr,
      command: "bsdkrun create (no machine id in output)"
    )
  end
  match = res.stderr.match(SSH_PORT_RE)
  new(id, ssh_port: match && match[1].to_i)
end

.get(id) ⇒ Sandbox

Reconnect to an existing machine by id (a unique prefix is enough).

Parameters:

  • id (String)

Returns:

Raises:



71
72
73
74
75
76
# File 'lib/bsdkrun/sandbox.rb', line 71

def get(id)
  row = list(all: true).find { |m| m.id == id || m.id.start_with?(id) }
  raise SandboxNotFound, id unless row

  new(row.id)
end

.list(all: false) ⇒ Array<SandboxInfo>

List machines. all: true includes exited ones (default running only).

Parameters:

  • all (Boolean) (defaults to: false)

Returns:



82
83
84
85
86
87
88
# File 'lib/bsdkrun/sandbox.rb', line 82

def list(all: false)
  args = ["ps", "--json"]
  args << "--all" if all
  res = Process.run!(args, label: "bsdkrun ps")
  rows = JSON.parse(res.stdout.empty? ? "[]" : res.stdout)
  rows.map { |row| SandboxInfo.from_row(row) }
end

Instance Method Details

#connect_network(network) ⇒ void

This method returns an undefined value.

Join or switch this machine to a global network. Applies on next #start.

Parameters:

  • network (String)


211
212
213
# File 'lib/bsdkrun/sandbox.rb', line 211

def connect_network(network)
  lifecycle(["network", "connect", @id, network], "bsdkrun network connect")
end

#disconnect_networkvoid

This method returns an undefined value.

Detach this machine from its network. Applies on next #start.



217
218
219
# File 'lib/bsdkrun/sandbox.rb', line 217

def disconnect_network
  lifecycle(["network", "disconnect", @id], "bsdkrun network disconnect")
end

#exec(command, args: [], env: {}, tty: false, stdin: nil, cwd: nil, throw_on_error: false, log_level: 0) ⇒ Result

Run a command in the guest through its exec agent.

command may be an Array (argv, no shell parsing) or a String program name; with a String, args: supplies its arguments.

Parameters:

  • command (String, Array<String>)
  • args (Array<String>) (defaults to: [])

    arguments when command is a bare String.

  • env (Hash) (defaults to: {})

    environment variables (+-e K=V+).

  • tty (Boolean) (defaults to: false)

    allocate a pseudo-TTY in the guest (+-t+).

  • stdin (String, nil) (defaults to: nil)

    data piped to the command's stdin.

  • cwd (String, nil) (defaults to: nil)

    working directory (emulated via sh -c 'cd …').

  • throw_on_error (Boolean) (defaults to: false)

    raise CommandFailed on a non-zero exit.

  • log_level (Integer) (defaults to: 0)

    per-command bsdkrun log level.

Returns:



113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/bsdkrun/sandbox.rb', line 113

def exec(command, args: [], env: {}, tty: false, stdin: nil, cwd: nil,
         throw_on_error: false, log_level: 0)
  argv = command.is_a?(Array) ? command.dup : [command, *args]

  if cwd
    # Emulate a working directory: cd, drop it, then exec the real argv.
    argv = ["/bin/sh", "-c", 'cd "$1" && shift && exec "$@"', "sh", cwd, *argv]
  end

  cli = ["exec"]
  cli << "-t" if tty
  env.each { |k, v| cli.push("-e", "#{k}=#{v}") }
  cli.push(@id, *argv)

  res = Process.run(cli, stdin: stdin, log_level: log_level)
  result = Result.new(
    stdout: res.stdout, stderr: res.stderr, exit_code: res.exit_code,
    command: "exec #{argv.join(' ')}"
  )
  result.throw_if_failed! if throw_on_error
  result
end

#logs(boot: false) ⇒ String

Read the machine's console log.

Parameters:

  • boot (Boolean) (defaults to: false)

    show bsdkrun's own boot log instead of the console.

Returns:

  • (String)


149
150
151
152
153
154
# File 'lib/bsdkrun/sandbox.rb', line 149

def logs(boot: false)
  args = ["logs"]
  args << "--boot" if boot
  args << @id
  Process.run(args).stdout
end

#remove(force: false) ⇒ void

This method returns an undefined value.

Remove the machine and its state. force stops it first if running.

Parameters:

  • force (Boolean) (defaults to: false)


190
191
192
193
194
195
# File 'lib/bsdkrun/sandbox.rb', line 190

def remove(force: false)
  args = ["rm"]
  args << "--force" if force
  args << @id
  lifecycle(args, "bsdkrun rm")
end

#run_command(command, args = [], **opts) ⇒ Result

Vercel-Sandbox-style alias for #exec: a program plus its args.

Parameters:

  • command (String)
  • args (Array<String>) (defaults to: [])

Returns:



141
142
143
# File 'lib/bsdkrun/sandbox.rb', line 141

def run_command(command, args = [], **opts)
  exec(command, args: args, **opts)
end

#running?Boolean

Whether the machine is currently running.

Returns:

  • (Boolean)


170
171
172
173
# File 'lib/bsdkrun/sandbox.rb', line 170

def running?
  s = status
  s ? s.running : false
end

#shellBoolean

Attach an interactive shell to the machine (inherits the terminal).

Returns:

  • (Boolean)

    true if the shell exited zero.



158
159
160
# File 'lib/bsdkrun/sandbox.rb', line 158

def shell
  Process.spawn_interactive(["shell", @id])
end

#ssh_setup(user: nil, key: nil) ⇒ Result

Install SSH keys in the guest via the agent (+ssh setup+). With no keys, the CLI installs your local ~/.ssh/*.pub.

Parameters:

  • user (String, nil) (defaults to: nil)

    target user (default root).

  • key (String, Array<String>, nil) (defaults to: nil)

    literal key(s) or .pub path(s).

Returns:



227
228
229
230
231
232
# File 'lib/bsdkrun/sandbox.rb', line 227

def ssh_setup(user: nil, key: nil)
  action = ["setup"]
  action.push("--user", user) if user
  Array(key).each { |k| action.push("--key", k) }
  agent("ssh", action)
end

#startvoid

This method returns an undefined value.

Restart a stopped machine in place (same id, disk/rootfs). Boots detached.



183
184
185
# File 'lib/bsdkrun/sandbox.rb', line 183

def start
  lifecycle(["start", @id], "bsdkrun start")
end

#statusSandboxInfo?

Fetch this machine's current status row, or nil if it's gone.

Returns:



164
165
166
# File 'lib/bsdkrun/sandbox.rb', line 164

def status
  Sandbox.list(all: true).find { |m| m.id == @id }
end

#stopvoid

This method returns an undefined value.

Stop the machine. BSD guests are cleanly powered off; Linux is SIGTERM'd.



177
178
179
# File 'lib/bsdkrun/sandbox.rb', line 177

def stop
  lifecycle(["stop", @id], "bsdkrun stop")
end

#tailscale_up(authkey: nil, hostname: nil, args: []) ⇒ Result

Put the guest on your tailnet (+tailscale setup+).

Parameters:

  • authkey (String, nil) (defaults to: nil)

    tailnet auth key (sent as TS_AUTHKEY).

  • hostname (String, nil) (defaults to: nil)

    machine name on the tailnet.

  • args (Array<String>) (defaults to: [])

    extra args passed through to tailscale up.

Returns:



240
241
242
243
244
245
# File 'lib/bsdkrun/sandbox.rb', line 240

def tailscale_up(authkey: nil, hostname: nil, args: [])
  action = ["setup"]
  action.push("--hostname", hostname) if hostname
  action.concat(args)
  agent("tailscale", action, env: authkey ? { "TS_AUTHKEY" => authkey } : {})
end

#update(cpus: nil, mem: nil) ⇒ void

This method returns an undefined value.

Change the recorded vCPU / RAM. Applies on the next #start.

Parameters:

  • cpus (Integer, nil) (defaults to: nil)
  • mem (Integer, nil) (defaults to: nil)


201
202
203
204
205
206
# File 'lib/bsdkrun/sandbox.rb', line 201

def update(cpus: nil, mem: nil)
  args = ["update", @id]
  args.push("--cpus", cpus.to_s) unless cpus.nil?
  args.push("--mem", mem.to_s) unless mem.nil?
  lifecycle(args, "bsdkrun update")
end