Class: Microsandbox::Sandbox

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

Overview

A running sandbox (microVM) — the primary entry point of the SDK.

Examples:

Block form (auto-stops on exit)

Microsandbox::Sandbox.create("hello", image: "python") do |sb|
  out = sb.exec("python", ["-c", "print('hi')"])
  puts out.stdout
end

Manual lifecycle

sb = Microsandbox::Sandbox.create("hello", image: "python")
begin
  sb.shell("echo hi")
ensure
  sb.stop
end

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(native) ⇒ Sandbox

Returns a new instance of Sandbox.



440
441
442
# File 'lib/microsandbox/sandbox.rb', line 440

def initialize(native)
  @native = native
end

Class Method Details

.create(name, image: nil, cpus: nil, memory: nil, env: nil, workdir: nil, shell: nil, user: nil, hostname: nil, labels: nil, scripts: nil, entrypoint: nil, ports: nil, ports_udp: nil, volumes: nil, network: nil, patches: nil, from_snapshot: nil, log_level: nil, quiet_logs: false, security: nil, oci_upper_size: nil, max_duration: nil, idle_timeout: nil, rlimits: nil, pull_policy: nil, registry_auth: nil, registry_insecure: false, registry_ca_certs: nil, secrets: nil, detached: false, replace: false, replace_with_timeout: nil) {|sandbox| ... } ⇒ Sandbox, Object

Create and boot a sandbox.

When a block is given the sandbox is yielded and stopped automatically when the block returns (the block’s value is returned); otherwise the live Microsandbox::Sandbox is returned and you are responsible for calling #stop.

Parameters:

  • name (String)

    sandbox name (max 128 UTF-8 bytes)

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

    OCI image reference (e.g. “python”)

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

    number of vCPUs

  • memory (Integer, nil) (defaults to: nil)

    memory in MiB

  • env (Hash, nil) (defaults to: nil)

    environment variables

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

    working directory inside the guest

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

    default shell (for #shell)

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

    default user

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

    guest hostname

  • labels (Hash, nil) (defaults to: nil)

    metadata labels

  • scripts (Hash, nil) (defaults to: nil)

    named scripts to install

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

    image entrypoint override

  • ports (Hash, nil) (defaults to: nil)

    host_port => guest_port TCP publications

  • ports_udp (Hash, nil) (defaults to: nil)

    host_port => guest_port UDP publications

  • network (String, Symbol, NetworkPolicy, Hash, nil) (defaults to: nil)

    network policy. A preset name (“public_only” (default), “none”, “allow_all”, “non_local”), a NetworkPolicy (e.g. NetworkPolicy.custom), or a Hash describing a custom policy (‘default_egress:`, `default_ingress:`, `rules:`, `deny_domains:`, `deny_domain_suffixes:`). See NetworkPolicy and Rule.

  • patches (Array<Hash>, nil) (defaults to: nil)

    rootfs patches applied before boot, each built with the Patch factory (e.g. ‘Patch.text(…)`, `Patch.mkdir(…)`). Not compatible with disk-image roots.

  • log_level ("error", "warn", "info", "debug", "trace", nil) (defaults to: nil)

    guest log verbosity

  • quiet_logs (Boolean) (defaults to: false)

    suppress sandbox process logs

  • security ("default", "restricted", nil) (defaults to: nil)

    exec security profile

  • oci_upper_size (Integer, nil) (defaults to: nil)

    writable upper-layer size cap, in MiB

  • max_duration (Integer, nil) (defaults to: nil)

    hard wall-clock lifetime, in seconds

  • idle_timeout (Integer, nil) (defaults to: nil)

    stop after this many idle seconds

  • rlimits (Hash, nil) (defaults to: nil)

    resource limits: { resource => limit } or { resource => [soft, hard] } (e.g. { nofile: 65_535 })

  • pull_policy ("always", "if-missing", "never", nil) (defaults to: nil)

    image pull behavior

  • registry_auth (Hash, nil) (defaults to: nil)

    credentials for a private/authenticated registry: { username:, password: } (the password may be a token). Without this the core’s default resolution chain still applies (OS keyring, global config, ‘~/.docker/config.json`).

  • registry_insecure (Boolean) (defaults to: false)

    reach the registry over plain HTTP instead of HTTPS (for local/self-hosted registries)

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

    extra PEM-encoded CA root certificate(s) to trust (for a registry with a private CA)

  • secrets (Array<Hash>, nil) (defaults to: nil)

    placeholder-protected secrets, each { env:, value:, host: } — the value is substituted by the TLS proxy only for the allowed host (auto-enables TLS interception)

  • detached (Boolean) (defaults to: false)

    keep running after this process exits

  • replace (Boolean) (defaults to: false)

    replace an existing sandbox with the same name

  • replace_with_timeout (Numeric, nil) (defaults to: nil)

    replace, waiting up to N seconds

Yield Parameters:

Returns:

  • (Sandbox, Object)

    the sandbox, or the block’s return value



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
# File 'lib/microsandbox/sandbox.rb', line 223

def create(name,
  image: nil, cpus: nil, memory: nil, env: nil, workdir: nil,
  shell: nil, user: nil, hostname: nil, labels: nil, scripts: nil,
  entrypoint: nil, ports: nil, ports_udp: nil, volumes: nil, network: nil,
  patches: nil,
  from_snapshot: nil, log_level: nil, quiet_logs: false, security: nil,
  oci_upper_size: nil, max_duration: nil, idle_timeout: nil, rlimits: nil,
  pull_policy: nil, registry_auth: nil, registry_insecure: false,
  registry_ca_certs: nil, secrets: nil,
  detached: false, replace: false, replace_with_timeout: nil)
  # A sandbox boots from exactly one rootfs source. The core would reject a
  # contradictory pair, but only after a runtime round-trip; fail fast and
  # clearly here (the Python SDK validates this the same way).
  if image && from_snapshot
    raise ArgumentError, "provide either image: or from_snapshot:, not both"
  end
  Microsandbox.ensure_runtime!
  opts = {}
  opts["image"] = image.to_s if image
  opts["from_snapshot"] = from_snapshot.to_s if from_snapshot
  opts["cpus"] = Integer(cpus) if cpus
  opts["memory"] = Integer(memory) if memory
  opts["workdir"] = workdir.to_s if workdir
  opts["shell"] = shell.to_s if shell
  opts["user"] = user.to_s if user
  opts["hostname"] = hostname.to_s if hostname
  opts["env"] = stringify(env) if env
  opts["labels"] = stringify(labels) if labels
  opts["scripts"] = stringify(scripts) if scripts
  opts["entrypoint"] = Array(entrypoint).map(&:to_s) if entrypoint
  opts["ports"] = intify_ports(ports) if ports
  opts["ports_udp"] = intify_ports(ports_udp) if ports_udp
  opts["volumes"] = normalize_volumes(volumes) if volumes
  opts["patches"] = normalize_patches(patches) if patches
  apply_network_opts(opts, network) unless network.nil?
  opts["log_level"] = log_level.to_s if log_level
  opts["quiet_logs"] = true if quiet_logs
  opts["security"] = security.to_s if security
  opts["oci_upper_size"] = Integer(oci_upper_size) if oci_upper_size
  opts["max_duration"] = Integer(max_duration) if max_duration
  opts["idle_timeout"] = Integer(idle_timeout) if idle_timeout
  opts["rlimits"] = normalize_rlimits(rlimits) if rlimits
  opts["pull_policy"] = pull_policy.to_s if pull_policy
  apply_registry_opts(opts, registry_auth, registry_insecure, registry_ca_certs)
  opts["secrets"] = normalize_secrets(secrets) if secrets
  opts["detached"] = true if detached
  if replace_with_timeout
    opts["replace_with_timeout"] = coerce_duration(replace_with_timeout, "replace_with_timeout")
  elsif replace
    opts["replace"] = true
  end

  sandbox = new(Native::Sandbox.create(name.to_s, opts))
  return sandbox unless block_given?

  begin
    yield sandbox
  ensure
    begin
      sandbox.stop
    rescue Microsandbox::Error
      # best-effort cleanup; ignore stop failures during teardown
    end
  end
end

.get(name) ⇒ SandboxHandle

Fetch a controllable handle for a sandbox by name (running or not).

Returns:



298
299
300
# File 'lib/microsandbox/sandbox.rb', line 298

def get(name)
  SandboxHandle.new(Native::Sandbox.get(name.to_s))
end

.listArray<SandboxHandle>

List all sandboxes as controllable handles.

Returns:



304
305
306
# File 'lib/microsandbox/sandbox.rb', line 304

def list
  Native::Sandbox.list.map { |h| SandboxHandle.new(h) }
end

.list_with(labels: {}) ⇒ Array<SandboxHandle>

List sandboxes carrying all of the given labels (AND-matched).

Parameters:

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

    required key => value labels

Returns:



311
312
313
314
# File 'lib/microsandbox/sandbox.rb', line 311

def list_with(labels: {})
  opts = {"labels" => stringify(labels)}
  Native::Sandbox.list_with(opts).map { |h| SandboxHandle.new(h) }
end

.remove(name) ⇒ nil

Remove a (stopped) sandbox by name.

Returns:

  • (nil)


318
319
320
321
# File 'lib/microsandbox/sandbox.rb', line 318

def remove(name)
  Native::Sandbox.remove(name.to_s)
  nil
end

.start(name, detached: false) ⇒ Sandbox

Restart a previously-defined sandbox by name.

Returns:



291
292
293
294
# File 'lib/microsandbox/sandbox.rb', line 291

def start(name, detached: false)
  Microsandbox.ensure_runtime!
  new(Native::Sandbox.start(name.to_s, {"detached" => detached}))
end

Instance Method Details

#attach(command, args = [], cwd: nil, user: nil, env: nil, detach_keys: nil, rlimits: nil) ⇒ Integer

Attach an interactive terminal to a command in the sandbox.

Puts the host terminal into raw mode and forwards keystrokes (and SIGWINCH resizes) to the guest until the command exits or the detach sequence is typed. Requires a real TTY on stdin/stdout, so it is for CLI use, not library/automation code (use #exec/#exec_stream there). Blocks until the session ends. Mirrors the official SDKs’ ‘attach`.

Parameters:

  • command (String)

    the program to run

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

    its arguments

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

    working directory

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

    user to run as

  • env (Hash, nil) (defaults to: nil)

    extra environment variables

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

    detach sequence (e.g. “ctrl-p,ctrl-q”; default “ctrl-]”)

  • rlimits (Hash, nil) (defaults to: nil)

    resource limits (see #exec)

Returns:

  • (Integer)

    the command’s exit code (or the code at detach)



510
511
512
513
514
515
516
517
518
519
520
521
522
523
# File 'lib/microsandbox/sandbox.rb', line 510

def attach(command, args = [], cwd: nil, user: nil, env: nil, detach_keys: nil, rlimits: nil)
  opts = {}
  opts["cwd"] = cwd.to_s if cwd
  opts["user"] = user.to_s if user
  opts["env"] = env.each_with_object({}) { |(k, v), a| a[k.to_s] = v.to_s } if env
  opts["detach_keys"] = detach_keys.to_s if detach_keys
  if rlimits
    opts["rlimits"] = rlimits.map do |resource, limit|
      soft, hard = limit.is_a?(Array) ? [limit[0], limit[1]] : [limit, limit]
      [resource.to_s, Integer(soft), Integer(hard)]
    end
  end
  @native.attach(command.to_s, Array(args).map(&:to_s), opts)
end

#attach_shellInteger

Attach an interactive terminal running the sandbox’s default shell. See #attach for the host-TTY requirements.

Returns:

  • (Integer)

    the shell’s exit code (or the code at detach)



528
529
530
# File 'lib/microsandbox/sandbox.rb', line 528

def attach_shell
  @native.attach_shell
end

#detachnil

Detach this handle: disarm the stop-on-drop safety net so the sandbox keeps running after this handle is gone (and after this process exits).

Returns:

  • (nil)


649
650
651
652
# File 'lib/microsandbox/sandbox.rb', line 649

def detach
  @native.detach
  nil
end

#drainnil

Trigger a graceful drain (SIGUSR1).

Returns:

  • (nil)


622
623
624
625
# File 'lib/microsandbox/sandbox.rb', line 622

def drain
  @native.drain
  nil
end

#exec(command, args = [], cwd: nil, user: nil, env: nil, timeout: nil, tty: false, stdin: nil, rlimits: nil) ⇒ ExecOutput

Run a command (no shell interpretation) and collect its output.

Parameters:

  • command (String)

    the executable

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

    arguments

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

    working directory

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

    user to run as

  • env (Hash, nil) (defaults to: nil)

    extra environment variables

  • timeout (Numeric, nil) (defaults to: nil)

    kill after N seconds

  • tty (Boolean) (defaults to: false)

    allocate a pseudo-terminal

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

    bytes to feed to stdin, or :pipe to open a streaming stdin pipe (write/close it via ExecHandle#stdin; only useful with the streaming variants)

Returns:



462
463
464
465
# File 'lib/microsandbox/sandbox.rb', line 462

def exec(command, args = [], cwd: nil, user: nil, env: nil, timeout: nil, tty: false, stdin: nil, rlimits: nil)
  ExecOutput.new(@native.exec(command.to_s, Array(args).map(&:to_s),
    exec_opts(cwd:, user:, env:, timeout:, tty:, stdin:, rlimits:)))
end

#exec_stream(command, args = [], cwd: nil, user: nil, env: nil, timeout: nil, tty: false, stdin: nil, rlimits: nil) ⇒ ExecHandle

Run a command and stream its output as it arrives.

Pass stdin: :pipe to feed the process interactively: ExecHandle#stdin then returns a writable sink; close it to send EOF (a process like cat that reads until EOF will otherwise block forever).

Returns:

See Also:



481
482
483
484
# File 'lib/microsandbox/sandbox.rb', line 481

def exec_stream(command, args = [], cwd: nil, user: nil, env: nil, timeout: nil, tty: false, stdin: nil, rlimits: nil)
  ExecHandle.new(@native.exec_stream(command.to_s, Array(args).map(&:to_s),
    exec_opts(cwd:, user:, env:, timeout:, tty:, stdin:, rlimits:, pipe_ok: true)))
end

#fsFS

Guest filesystem operations.

Returns:



534
535
536
# File 'lib/microsandbox/sandbox.rb', line 534

def fs
  @fs ||= FS.new(@native)
end

#inspectObject



654
655
656
# File 'lib/microsandbox/sandbox.rb', line 654

def inspect
  "#<Microsandbox::Sandbox name=#{name.inspect}>"
end

#killnil

Force-kill the sandbox (SIGKILL).

Returns:

  • (nil)


615
616
617
618
# File 'lib/microsandbox/sandbox.rb', line 615

def kill
  @native.kill
  nil
end

#log_stream(sources: nil, since_ms: nil, from_cursor: nil, until_ms: nil, follow: false) ⇒ LogStream

Stream captured logs as they appear.

Parameters:

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

    filter by source (“stdout”/“stderr”/“output”/“system”)

  • since_ms (Numeric, nil) (defaults to: nil)

    start at the first entry at/after this Unix ms

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

    resume exactly after a prior LogEntry#cursor (mutually exclusive with since_ms; takes precedence if both given)

  • until_ms (Numeric, nil) (defaults to: nil)

    stop before any entry at/after this Unix ms

  • follow (Boolean) (defaults to: false)

    keep the stream open for new entries past current EOF

Returns:



588
589
590
591
592
593
594
595
596
# File 'lib/microsandbox/sandbox.rb', line 588

def log_stream(sources: nil, since_ms: nil, from_cursor: nil, until_ms: nil, follow: false)
  opts = {}
  opts["sources"] = Array(sources).map(&:to_s) if sources
  opts["since_ms"] = Float(since_ms) if since_ms
  opts["from_cursor"] = from_cursor.to_s if from_cursor
  opts["until_ms"] = Float(until_ms) if until_ms
  opts["follow"] = true if follow
  LogStream.new(@native.log_stream(opts))
end

#logs(tail: nil, since_ms: nil, until_ms: nil, sources: nil) ⇒ Array<LogEntry>

Read captured logs.

Parameters:

  • tail (Integer, nil) (defaults to: nil)

    only the last N entries

  • since_ms (Numeric, nil) (defaults to: nil)

    only entries at/after this Unix ms

  • until_ms (Numeric, nil) (defaults to: nil)

    only entries before this Unix ms

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

    filter by source (“stdout”/“stderr”/“output”/“system”/“all”)

Returns:



561
562
563
564
565
566
567
568
# File 'lib/microsandbox/sandbox.rb', line 561

def logs(tail: nil, since_ms: nil, until_ms: nil, sources: nil)
  opts = {}
  opts["tail"] = Integer(tail) if tail
  opts["since_ms"] = Float(since_ms) if since_ms
  opts["until_ms"] = Float(until_ms) if until_ms
  opts["sources"] = Array(sources).map(&:to_s) if sources
  @native.logs(opts).map { |entry| LogEntry.new(entry) }
end

#metricsMetrics

Latest resource-usage snapshot.

Returns:



549
550
551
# File 'lib/microsandbox/sandbox.rb', line 549

def metrics
  Metrics.new(@native.metrics)
end

#metrics_stream(interval: 1.0) ⇒ MetricsStream

Stream resource-usage snapshots, one per interval tick, until the sandbox stops. Requires metrics to be enabled for the sandbox.

Parameters:

  • interval (Numeric) (defaults to: 1.0)

    seconds between snapshots

Returns:



574
575
576
# File 'lib/microsandbox/sandbox.rb', line 574

def metrics_stream(interval: 1.0)
  MetricsStream.new(@native.metrics_stream(coerce_duration(interval, "interval")))
end

#nameString

Returns the sandbox name.

Returns:

  • (String)

    the sandbox name



445
446
447
# File 'lib/microsandbox/sandbox.rb', line 445

def name
  @native.name
end

#owns_lifecycle?Boolean

Returns whether this handle owns the sandbox process lifecycle (i.e. stopping it or dropping the handle terminates the sandbox).

Returns:

  • (Boolean)

    whether this handle owns the sandbox process lifecycle (i.e. stopping it or dropping the handle terminates the sandbox)



642
643
644
# File 'lib/microsandbox/sandbox.rb', line 642

def owns_lifecycle?
  @native.owns_lifecycle
end

#shell(script, cwd: nil, user: nil, env: nil, timeout: nil, tty: false, stdin: nil, rlimits: nil) ⇒ ExecOutput

Run a shell script (pipes, redirects, etc. allowed) and collect output.

Returns:



469
470
471
472
# File 'lib/microsandbox/sandbox.rb', line 469

def shell(script, cwd: nil, user: nil, env: nil, timeout: nil, tty: false, stdin: nil, rlimits: nil)
  ExecOutput.new(@native.shell(script.to_s,
    exec_opts(cwd:, user:, env:, timeout:, tty:, stdin:, rlimits:)))
end

#shell_stream(script, cwd: nil, user: nil, env: nil, timeout: nil, tty: false, stdin: nil, rlimits: nil) ⇒ ExecHandle

Run a shell script and stream its output as it arrives.

Returns:



488
489
490
491
# File 'lib/microsandbox/sandbox.rb', line 488

def shell_stream(script, cwd: nil, user: nil, env: nil, timeout: nil, tty: false, stdin: nil, rlimits: nil)
  ExecHandle.new(@native.shell_stream(script.to_s,
    exec_opts(cwd:, user:, env:, timeout:, tty:, stdin:, rlimits:, pipe_ok: true)))
end

#sshSshOps

SSH access to the sandbox — open a native in-process SSH client or prepare a reusable server endpoint.

Examples:

sb.ssh.open_client { |c| puts c.exec("hostname").stdout }

Returns:



543
544
545
# File 'lib/microsandbox/sandbox.rb', line 543

def ssh
  SshOps.new(@native)
end

#statusSymbol

The live status, fetched from the backend (a round-trip per call).

Returns:

  • (Symbol)

    :created, :starting, :running, :draining, :paused, :stopped, or :crashed



636
637
638
# File 'lib/microsandbox/sandbox.rb', line 636

def status
  @native.status.to_sym
end

#stopnil

Gracefully stop the sandbox (SIGTERM→SIGKILL escalation, 10s default) and wait for it to terminate. For a custom timeout or fire-and-return ‘request_*` control, fetch a Microsandbox::SandboxHandle via get.

Returns:

  • (nil)


602
603
604
605
# File 'lib/microsandbox/sandbox.rb', line 602

def stop
  @native.stop
  nil
end

#stop_and_waitExitStatus

Gracefully stop, then wait for the process to exit.

Returns:



609
610
611
# File 'lib/microsandbox/sandbox.rb', line 609

def stop_and_wait
  ExitStatus.new(@native.stop_and_wait)
end

#waitExitStatus

Wait for the sandbox process to exit.

Returns:



629
630
631
# File 'lib/microsandbox/sandbox.rb', line 629

def wait
  ExitStatus.new(@native.wait)
end