bsdkrun 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f3cc7fd1dcbcb90243855217c4fe7852f6e98baf235fc6e0875ca3f7aeafeb7b
4
+ data.tar.gz: 92aaa16edb2d6970912f64840835dbd78611cf9f30bc4c5830c1c4f0f537fa71
5
+ SHA512:
6
+ metadata.gz: 1505af9b58a1397e6de294f06d76b2a8ca81ae0a46ba80f61c12baa6e5de99d8e9fba3c973bee7157c663233e575468bbe175e435fe23a01258855a4278a00fa
7
+ data.tar.gz: 38b533ac4692529528ce0c5c5739b1f62f8012f7ec6a2ac2814f866c99d7ec47a13ab26f1639e102012ca4e8161483cfe0731c8d987c23432da42133dfc61167
data/README.md ADDED
@@ -0,0 +1,218 @@
1
+ # bsdkrun (Ruby SDK)
2
+
3
+ A Ruby SDK for [**bsdkrun**](https://github.com/tsirysndr/bsdkrun) — a
4
+ Firecracker-style microVM launcher for **BSD and Linux** guests on macOS and
5
+ Linux, built on [libkrun](https://github.com/containers/libkrun). Boot and drive
6
+ microVMs programmatically, inspired by the **Vercel** and **Deno** Sandbox SDKs.
7
+
8
+ The SDK shells out to the `bsdkrun` binary, so it has **zero runtime
9
+ dependencies** — just the Ruby standard library (`open3`, `json`, `pathname`).
10
+
11
+ ```ruby
12
+ require "bsdkrun"
13
+
14
+ box = Bsdkrun::Sandbox.create(os: "linux", image: "alpine")
15
+
16
+ # exec argv directly, with env / stdin / a PTY / a working dir:
17
+ puts box.exec(["uname", "-a"]).text
18
+ box.exec(["apk", "add", "curl"], throw_on_error: true)
19
+ box.run_command("curl", ["-fsSL", "https://example.com"])
20
+
21
+ box.stop
22
+ ```
23
+
24
+ ## Install
25
+
26
+ ```sh
27
+ gem install bsdkrun
28
+ ```
29
+
30
+ Or in a `Gemfile`:
31
+
32
+ ```ruby
33
+ gem "bsdkrun"
34
+ ```
35
+
36
+ ### The `bsdkrun` binary
37
+
38
+ You need the `bsdkrun` binary itself. The SDK finds it via, in order:
39
+
40
+ 1. `Bsdkrun.binary_path = "/path/to/bsdkrun"`
41
+ 2. the `BSDKRUN_BIN` environment variable
42
+ 3. `bsdkrun` on your `PATH`
43
+ 4. an in-repo `target/release/bsdkrun` or `target/debug/bsdkrun` build
44
+
45
+ See the [bsdkrun README](../../README.md) for installing the binary (Homebrew on
46
+ macOS, or build from source on Linux/KVM). This SDK assumes libkrun is already
47
+ linked — it does not auto-provision it.
48
+
49
+ ## Creating a sandbox
50
+
51
+ `Sandbox.create` is discriminated on `os:` — the options change per guest kind.
52
+ Pass keyword arguments or a Hash.
53
+
54
+ ```ruby
55
+ # Linux OCI image (docker run-style)
56
+ Bsdkrun::Sandbox.create(
57
+ os: "linux",
58
+ image: "ghcr.io/owner/name:tag",
59
+ cpus: 2,
60
+ mem: 1024,
61
+ volume: "web", # persistent CoW rootfs
62
+ mounts: ["~/project:/src", "~/data:/data:ro"],
63
+ net: { ports: ["8080:80", "2222:22"] },
64
+ command: ["node", "server.js"] # args after `--`
65
+ )
66
+
67
+ # FreeBSD (EFI on macOS, PVH on Linux/amd64)
68
+ Bsdkrun::Sandbox.create(os: "freebsd", version: "14.3", mem: 2048)
69
+
70
+ # NetBSD (direct-kernel boot everywhere)
71
+ Bsdkrun::Sandbox.create(os: "netbsd", version: "10.1", volume: "db")
72
+
73
+ # Boot a raw disk through its UEFI loader
74
+ Bsdkrun::Sandbox.create(os: "firmware", firmware: "KRUN_EFI.fd", disk: "disk.raw")
75
+
76
+ # Boot a kernel directly, no bootloader
77
+ Bsdkrun::Sandbox.create(os: "kernel", kernel: "netbsd", format: "elf", disk: "root.raw")
78
+ ```
79
+
80
+ Every `create` runs the machine **detached** and returns a `Sandbox` handle.
81
+
82
+ ## Running commands
83
+
84
+ `exec` is the primary programmatic entrypoint. No shell parsing — pass an argv
85
+ array (or a program name plus `args:`).
86
+
87
+ ```ruby
88
+ box.exec(["ls", "-la", "/etc"])
89
+
90
+ box.exec("ruby",
91
+ args: ["-e", "puts ENV['X']"],
92
+ env: { "X" => "hi" },
93
+ cwd: "/app",
94
+ stdin: "data on stdin",
95
+ tty: true, # allocate a PTY
96
+ throw_on_error: true) # raise on non-zero exit (default: false)
97
+
98
+ # Vercel-Sandbox-style alias:
99
+ result = box.run_command("uname", ["-a"])
100
+ result.stdout # raw stdout
101
+ result.text # stdout, trailing newlines trimmed
102
+ result.exit_code
103
+ result.ok? # true on exit 0
104
+ result.lines # non-empty stdout lines
105
+ ```
106
+
107
+ `exec` returns a `Bsdkrun::Result`. It only raises `CommandFailed` when you pass
108
+ `throw_on_error: true` (or call `result.throw_if_failed!`).
109
+
110
+ ## Lifecycle & inventory
111
+
112
+ ```ruby
113
+ box = Bsdkrun::Sandbox.create(os: "linux", image: "alpine", command: ["sleep", "300"])
114
+ same = Bsdkrun::Sandbox.get(box.id) # reconnect (prefix ok)
115
+ all = Bsdkrun::Sandbox.list(all: true) # Array<SandboxInfo>
116
+
117
+ box.status # SandboxInfo | nil
118
+ box.running? # true / false
119
+ box.logs # console log (String)
120
+ box.shell # interactive shell (inherits the terminal)
121
+ box.stop # BSD guests clean-poweroff; Linux SIGTERM
122
+ box.start # restart in place — resumes its own disk/rootfs (data persists)
123
+ box.update(cpus: 4, mem: 2048) # applies on next start
124
+ box.remove(force: true)
125
+ ```
126
+
127
+ Host-level namespaces:
128
+
129
+ ```ruby
130
+ Bsdkrun::System.probe # toolchain sanity check -> Boolean
131
+ Bsdkrun::Images.list # Array<ImageInfo>
132
+ Bsdkrun::Volumes.list # Array<VolumeInfo>
133
+ Bsdkrun::Volumes.remove("web", force: true)
134
+ Bsdkrun::Networks.list # Array<NetworkInfo>
135
+ Bsdkrun::System.fetch_image("freebsd", version: "14.3")
136
+ Bsdkrun::System.versions("netbsd") # Array<String>
137
+ ```
138
+
139
+ ## Networking, SSH & Tailscale
140
+
141
+ ```ruby
142
+ # forward ports at create time
143
+ Bsdkrun::Sandbox.create(os: "linux", image: "alpine", net: { ports: ["2222:22"] })
144
+
145
+ # agent-managed key-based SSH
146
+ box.ssh_setup # install local ~/.ssh/*.pub keys
147
+ box.ssh_setup(user: "tsiry", key: "~/.ssh/work.pub")
148
+
149
+ # put a guest on your tailnet
150
+ box.tailscale_up(authkey: "tskey-auth-...", hostname: "web")
151
+ ```
152
+
153
+ ### Global networks — reach machines by name
154
+
155
+ Opt machines into a **shared network** so they get distinct IPs on one subnet
156
+ and reach each other **by IP and by name** (docker-compose style), with internal
157
+ DNS:
158
+
159
+ ```ruby
160
+ require "bsdkrun"
161
+
162
+ Bsdkrun::Networks.create("devnet")
163
+
164
+ db = Bsdkrun::Sandbox.create(
165
+ os: "linux", image: "alpine", name: "db",
166
+ net: { network: "devnet" }, command: ["sleep", "3600"]
167
+ )
168
+ api = Bsdkrun::Sandbox.create(
169
+ os: "linux", image: "alpine", name: "api",
170
+ net: { network: "devnet" }, command: ["sleep", "3600"]
171
+ )
172
+
173
+ # api reaches db by name over the shared subnet
174
+ api.exec(["ping", "-c1", "db"], throw_on_error: true)
175
+
176
+ # inspect + manage
177
+ Bsdkrun::Networks.list # Array<NetworkInfo>
178
+ Bsdkrun::Networks.members("devnet") # Array<SandboxInfo> on the network
179
+ info = db.status # info.network == "devnet", info.net_ip set
180
+
181
+ # edit membership (applies on next start — a VM's NIC is fixed at boot)
182
+ api.connect_network("devnet") # or Bsdkrun::Networks.connect(api.id, "devnet")
183
+ api.disconnect_network
184
+ api.start # re-joins with the new membership
185
+
186
+ Bsdkrun::Networks.sync("devnet") # refresh members' /etc/hosts (fixes NetBSD name lookup)
187
+ Bsdkrun::Networks.remove("devnet", force: true)
188
+ ```
189
+
190
+ Names resolve on Linux and FreeBSD via the network's DNS; **NetBSD** resolves
191
+ via a synced `/etc/hosts` block — joins auto-sync, and `Networks.sync` refreshes
192
+ an existing network without restarting members.
193
+
194
+ ## Errors
195
+
196
+ All errors extend `Bsdkrun::Error`:
197
+
198
+ - `Bsdkrun::BinaryNotFound` — the `bsdkrun` binary wasn't found.
199
+ - `Bsdkrun::CommandFailed` — a command exited non-zero (carries `exit_code`,
200
+ `stdout`, `stderr`, `command`). Raised by `exec` with `throw_on_error: true`,
201
+ by the lifecycle/namespace helpers, and by the agent helpers.
202
+ - `Bsdkrun::SandboxNotFound` — `Sandbox.get` matched no machine.
203
+
204
+ ## Try it interactively
205
+
206
+ ```sh
207
+ bin/console
208
+ ```
209
+
210
+ Starts IRB with the SDK preloaded — `Bsdkrun::Sandbox`, the `Bsdkrun.images` /
211
+ `.volumes` / `.networks` / `.system` namespaces, plus `ps` (every machine,
212
+ exited ones included) and `last` (the newest one). Pass
213
+ `--bin ../../target/release/bsdkrun` to drive a locally built binary for the
214
+ session.
215
+
216
+ ## License
217
+
218
+ MIT
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bsdkrun
4
+ # Builds the +bsdkrun+ argv (minus the binary and global flags) for a detached
5
+ # +create+. Every path ends with +-d+ so +create+ yields a handle.
6
+ #
7
+ # Options are a Hash with Symbol keys mirroring the TypeScript SDK's
8
+ # +CreateOptions+, discriminated on +:os+.
9
+ module Args
10
+ module_function
11
+
12
+ # Networking flags shared by every guest kind.
13
+ # @param net [Hash, nil]
14
+ # @return [Array<String>]
15
+ def net_args(net)
16
+ a = []
17
+ return a unless net
18
+
19
+ a.push("--no-net") if net[:disabled]
20
+ Array(net[:ports]).each do |p|
21
+ a.push("--port", p.is_a?(String) ? p : "#{p[:host]}:#{p[:guest]}")
22
+ end
23
+ a.push("--mac", net[:mac]) if net[:mac]
24
+ a.push("--network", net[:network]) if net[:network]
25
+ a
26
+ end
27
+
28
+ # +--name+ flag if a name is set.
29
+ # @param o [Hash]
30
+ # @return [Array<String>]
31
+ def name_args(o)
32
+ o[:name] ? ["--name", o[:name].to_s] : []
33
+ end
34
+
35
+ # +--cpus+ / +--mem+ sizing flags.
36
+ # @param o [Hash]
37
+ # @return [Array<String>]
38
+ def vm_args(o)
39
+ a = []
40
+ a.push("--cpus", o[:cpus].to_s) unless o[:cpus].nil?
41
+ a.push("--mem", o[:mem].to_s) unless o[:mem].nil?
42
+ a
43
+ end
44
+
45
+ # Disk-persistence flags shared by BSD / firmware / kernel guests.
46
+ # @param o [Hash]
47
+ # @return [Array<String>]
48
+ def disk_args(o)
49
+ a = []
50
+ a.push("--persist") if o[:persist]
51
+ a.push("-v", o[:volume]) if o[:volume]
52
+ Array(o[:attach_disk]).each { |d| a.push("--attach-disk", d) }
53
+ a
54
+ end
55
+
56
+ # Build the full detached +create+ argv for the given options.
57
+ #
58
+ # @param opts [Hash] create options, discriminated on +:os+.
59
+ # @return [Array<String>]
60
+ # @raise [ArgumentError] on an unknown +:os+.
61
+ def build_create_args(opts)
62
+ case opts[:os].to_s
63
+ when "linux" then linux_args(opts)
64
+ when "freebsd" then freebsd_args(opts)
65
+ when "netbsd" then netbsd_args(opts)
66
+ when "firmware" then firmware_args(opts)
67
+ when "kernel" then kernel_args(opts)
68
+ else raise ArgumentError, "unknown os: #{opts[:os].inspect}"
69
+ end
70
+ end
71
+
72
+ # @!visibility private
73
+ def linux_args(opts)
74
+ a = ["linux", opts.fetch(:image).to_s, "-d"]
75
+ a.push("--kernel", opts[:kernel]) if opts[:kernel]
76
+ a.push("--kernel-version", opts[:kernel_version]) if opts[:kernel_version]
77
+ a.push("--initramfs") if opts[:initramfs]
78
+ a.push("-v", opts[:volume]) if opts[:volume]
79
+ Array(opts[:mounts]).each { |m| a.push("--mount", m) }
80
+ a.push("--entrypoint", opts[:entrypoint]) if opts[:entrypoint]
81
+ a.push("--console", opts[:console]) if opts[:console]
82
+ a.concat(net_args(opts[:net])).concat(name_args(opts)).concat(vm_args(opts))
83
+ cmd = opts[:command]
84
+ a.push("--", *cmd) if cmd && !cmd.empty?
85
+ a
86
+ end
87
+
88
+ # @!visibility private
89
+ def freebsd_args(opts)
90
+ a = ["freebsd", "-d"]
91
+ a.push("--version", opts[:version].to_s) if opts[:version]
92
+ a.push("--firmware", opts[:firmware]) if opts[:firmware]
93
+ a.push("--force") if opts[:force]
94
+ a.concat(disk_args(opts)).concat(net_args(opts[:net]))
95
+ .concat(name_args(opts)).concat(vm_args(opts))
96
+ end
97
+
98
+ # @!visibility private
99
+ def netbsd_args(opts)
100
+ a = ["netbsd", "-d"]
101
+ a.push("--version", opts[:version].to_s) if opts[:version]
102
+ a.push("--force") if opts[:force]
103
+ a.concat(disk_args(opts)).concat(net_args(opts[:net]))
104
+ .concat(name_args(opts)).concat(vm_args(opts))
105
+ end
106
+
107
+ # @!visibility private
108
+ def firmware_args(opts)
109
+ a = ["firmware", "--firmware", opts.fetch(:firmware), "--disk", opts.fetch(:disk), "-d"]
110
+ a.concat(disk_args(opts)).concat(net_args(opts[:net]))
111
+ .concat(name_args(opts)).concat(vm_args(opts))
112
+ end
113
+
114
+ # @!visibility private
115
+ def kernel_args(opts)
116
+ a = ["kernel", "--kernel", opts.fetch(:kernel), "-d"]
117
+ a.push("--format", opts[:format].to_s) if opts[:format]
118
+ a.push("--initramfs", opts[:initramfs]) if opts[:initramfs]
119
+ a.push("--cmdline", opts[:cmdline]) if opts[:cmdline]
120
+ a.push("--disk", opts[:disk]) if opts[:disk]
121
+ a.concat(disk_args(opts)).concat(net_args(opts[:net]))
122
+ .concat(name_args(opts)).concat(vm_args(opts))
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+
5
+ module Bsdkrun
6
+ # Locates the +bsdkrun+ binary on the host and caches the result.
7
+ #
8
+ # Resolution order (first match wins):
9
+ # 1. an explicit override set via {Bsdkrun.binary_path=}
10
+ # 2. the +BSDKRUN_BIN+ environment variable
11
+ # 3. +bsdkrun+ on +PATH+
12
+ # 4. an in-repo dev build: +<repo>/target/release/bsdkrun+ then +debug+
13
+ module Binary
14
+ @override = nil
15
+ @resolved = nil
16
+
17
+ class << self
18
+ # Force the SDK to use a specific +bsdkrun+ binary, bypassing discovery.
19
+ #
20
+ # @param path [String, nil]
21
+ # @return [void]
22
+ def override=(path)
23
+ @override = path
24
+ @resolved = nil
25
+ end
26
+
27
+ # @return [String, nil] the current explicit override, if any.
28
+ attr_reader :override
29
+
30
+ # Reset cached discovery state (mainly for tests).
31
+ # @return [void]
32
+ def reset!
33
+ @resolved = nil
34
+ @override = nil
35
+ end
36
+
37
+ # Resolve (and cache) the path to the +bsdkrun+ binary.
38
+ #
39
+ # @return [String] the resolved absolute path or bare command name.
40
+ # @raise [BinaryNotFound] when nothing matched.
41
+ def resolve
42
+ return @resolved if @resolved
43
+
44
+ searched = candidates
45
+ searched.each do |candidate|
46
+ if path_like?(candidate)
47
+ return @resolved = candidate if File.exist?(candidate)
48
+ else
49
+ found = which(candidate)
50
+ return @resolved = found if found
51
+ end
52
+ end
53
+ raise BinaryNotFound, searched
54
+ end
55
+
56
+ private
57
+
58
+ def path_like?(candidate)
59
+ candidate.include?(File::SEPARATOR) ||
60
+ (File::ALT_SEPARATOR && candidate.include?(File::ALT_SEPARATOR))
61
+ end
62
+
63
+ # Candidate locations, in priority order.
64
+ def candidates
65
+ out = []
66
+ out << @override if @override
67
+
68
+ env = ENV["BSDKRUN_BIN"]
69
+ out << env if env && !env.empty?
70
+
71
+ # A `bsdkrun` already on PATH wins over in-repo builds.
72
+ on_path = which("bsdkrun")
73
+ out << on_path if on_path
74
+
75
+ # This file lives at sdk/ruby/lib/bsdkrun/binary.rb, so the repo root is
76
+ # four levels up. Prefer a release build, fall back to debug.
77
+ repo_root = File.expand_path("../../../..", __dir__)
78
+ out << File.join(repo_root, "target", "release", "bsdkrun")
79
+ out << File.join(repo_root, "target", "debug", "bsdkrun")
80
+ out
81
+ end
82
+
83
+ # Cross-platform PATH lookup for an executable.
84
+ def which(name)
85
+ exts =
86
+ if File::ALT_SEPARATOR
87
+ (ENV["PATHEXT"] || ".EXE;.CMD;.BAT").split(";")
88
+ else
89
+ [""]
90
+ end
91
+ (ENV["PATH"] || "").split(File::PATH_SEPARATOR).each do |dir|
92
+ next if dir.empty?
93
+
94
+ exts.each do |ext|
95
+ full = File.join(dir, name + ext)
96
+ return full if File.file?(full) && File.executable?(full)
97
+ end
98
+ end
99
+ nil
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bsdkrun
4
+ # Base class for every error the SDK raises.
5
+ class Error < StandardError; end
6
+
7
+ # The +bsdkrun+ binary could not be located on the host.
8
+ class BinaryNotFound < Error
9
+ # @param searched [Array<String>] the paths that were probed, in order.
10
+ def initialize(searched)
11
+ super(
12
+ 'could not find the "bsdkrun" binary. Set BSDKRUN_BIN, add it to PATH, ' \
13
+ "or set Bsdkrun.binary_path=. Looked in: #{Array(searched).join(', ')}"
14
+ )
15
+ end
16
+ end
17
+
18
+ # A +bsdkrun+ invocation exited non-zero.
19
+ class CommandFailed < Error
20
+ # @return [Integer] the process exit status.
21
+ attr_reader :exit_code
22
+ # @return [String] the captured standard output.
23
+ attr_reader :stdout
24
+ # @return [String] the captured standard error.
25
+ attr_reader :stderr
26
+ # @return [String] a human label for the command that failed.
27
+ attr_reader :command
28
+
29
+ # @param exit_code [Integer]
30
+ # @param stdout [String]
31
+ # @param stderr [String]
32
+ # @param command [String] label describing the invocation.
33
+ def initialize(exit_code:, stdout:, stderr:, command:)
34
+ @exit_code = exit_code
35
+ @stdout = stdout
36
+ @stderr = stderr
37
+ @command = command
38
+ message = "command failed (exit #{exit_code}): #{command}"
39
+ trimmed = stderr.to_s.strip
40
+ message += "\n#{trimmed}" unless trimmed.empty?
41
+ super(message)
42
+ end
43
+ end
44
+
45
+ # No machine matched the given id / prefix.
46
+ class SandboxNotFound < Error
47
+ # @param id [String] the id or prefix that matched nothing.
48
+ def initialize(id)
49
+ super("no sandbox found matching id #{id.inspect}")
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Bsdkrun
6
+ # Host-level image operations.
7
+ module Images
8
+ module_function
9
+
10
+ # List downloaded images (pulled OCI images + fetched BSD images).
11
+ # @return [Array<ImageInfo>]
12
+ def list
13
+ res = Process.run!(["images", "--json"], label: "bsdkrun images")
14
+ rows = JSON.parse(res.stdout.empty? ? "[]" : res.stdout)
15
+ rows.map { |row| ImageInfo.from_row(row) }
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Bsdkrun
6
+ # Global-network operations — shared subnets where members reach each other by
7
+ # IP and by name.
8
+ module Networks
9
+ module_function
10
+
11
+ # List global networks and their member counts.
12
+ # @return [Array<NetworkInfo>]
13
+ def list
14
+ res = Process.run!(["network", "ls", "--json"], label: "bsdkrun network ls")
15
+ rows = JSON.parse(res.stdout.empty? ? "[]" : res.stdout)
16
+ rows.map { |row| NetworkInfo.from_row(row) }
17
+ end
18
+
19
+ # Create a global network (starts its shared switch).
20
+ # @param name [String]
21
+ # @return [void]
22
+ def create(name)
23
+ Process.run!(["network", "create", name], label: "bsdkrun network create")
24
+ nil
25
+ end
26
+
27
+ # Remove one or more networks. +force+ allows removal with running members.
28
+ # @param names [String, Array<String>]
29
+ # @param force [Boolean]
30
+ # @return [void]
31
+ def remove(names, force: false)
32
+ args = ["network", "rm"]
33
+ args << "--force" if force
34
+ args.concat(Array(names))
35
+ Process.run!(args, label: "bsdkrun network rm")
36
+ nil
37
+ end
38
+
39
+ # Join or switch a machine to a network. Applies on the machine's next start.
40
+ # @param machine [String] id or name.
41
+ # @param network [String]
42
+ # @return [void]
43
+ def connect(machine, network)
44
+ Process.run!(["network", "connect", machine, network], label: "bsdkrun network connect")
45
+ nil
46
+ end
47
+
48
+ # Detach a machine from its network. Applies on its next start.
49
+ # @param machine [String]
50
+ # @return [void]
51
+ def disconnect(machine)
52
+ Process.run!(["network", "disconnect", machine], label: "bsdkrun network disconnect")
53
+ nil
54
+ end
55
+
56
+ # Refresh members' +/etc/hosts+ so peers resolve by name (notably NetBSD).
57
+ # @param network [String]
58
+ # @return [void]
59
+ def sync(network)
60
+ Process.run!(["network", "sync", network], label: "bsdkrun network sync")
61
+ nil
62
+ end
63
+
64
+ # The machines currently attached to +network+ (running or stopped).
65
+ # @param network [String]
66
+ # @return [Array<SandboxInfo>]
67
+ def members(network)
68
+ Sandbox.list(all: true).select { |m| m.network == network }
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+
5
+ module Bsdkrun
6
+ # Spawns the +bsdkrun+ CLI and captures its output.
7
+ #
8
+ # Every invocation is prefixed with +--log-level+ (default 0) so the SDK's
9
+ # captured output stays clean.
10
+ module Process
11
+ # Captured output of a raw CLI invocation.
12
+ #
13
+ # @!attribute [r] stdout
14
+ # @return [String]
15
+ # @!attribute [r] stderr
16
+ # @return [String]
17
+ # @!attribute [r] exit_code
18
+ # @return [Integer]
19
+ RawResult = Struct.new(:stdout, :stderr, :exit_code, keyword_init: true)
20
+
21
+ module_function
22
+
23
+ # Run +bsdkrun --log-level <n> <args>+ to completion, buffering output.
24
+ #
25
+ # @param args [Array<String>] CLI arguments (without the binary).
26
+ # @param env [Hash] extra environment merged onto the process env.
27
+ # @param stdin [String, nil] data piped to the child's stdin.
28
+ # @param log_level [Integer] bsdkrun global log level (0=off .. 5=trace).
29
+ # @return [RawResult]
30
+ def run(args, env: {}, stdin: nil, log_level: 0)
31
+ bin = Binary.resolve
32
+ full = ["--log-level", log_level.to_s, *args]
33
+ merged_env = env.to_h.transform_keys(&:to_s).transform_values(&:to_s)
34
+ out, err, status = Open3.capture3(merged_env, bin, *full, stdin_data: stdin || "")
35
+ RawResult.new(stdout: out, stderr: err, exit_code: status.exitstatus || 0)
36
+ end
37
+
38
+ # Run and raise {CommandFailed} on a non-zero exit.
39
+ #
40
+ # @param args [Array<String>]
41
+ # @param label [String] human label used in the error.
42
+ # @return [RawResult]
43
+ # @raise [CommandFailed]
44
+ def run!(args, label:, **opts)
45
+ res = run(args, **opts)
46
+ unless res.exit_code.zero?
47
+ raise CommandFailed.new(
48
+ exit_code: res.exit_code, stdout: res.stdout, stderr: res.stderr, command: label
49
+ )
50
+ end
51
+ res
52
+ end
53
+
54
+ # Spawn an interactive +bsdkrun+ command inheriting the parent's stdio and
55
+ # wait for it (for +shell+). Returns the child's exit status boolean.
56
+ #
57
+ # @param args [Array<String>]
58
+ # @param log_level [Integer]
59
+ # @return [Boolean] true if the command exited zero.
60
+ def spawn_interactive(args, log_level: 0)
61
+ bin = Binary.resolve
62
+ full = ["--log-level", log_level.to_s, *args]
63
+ system(bin, *full)
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,265 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Bsdkrun
6
+ # A handle to a running (or stopped) bsdkrun microVM.
7
+ #
8
+ # Create one with {Sandbox.create}, reconnect with {Sandbox.get}, or enumerate
9
+ # with {Sandbox.list}.
10
+ #
11
+ # @example
12
+ # box = Bsdkrun::Sandbox.create(os: "linux", image: "alpine")
13
+ # box.exec(["uname", "-a"]).text
14
+ # box.stop
15
+ class Sandbox
16
+ ID_RE = /\A[0-9a-f]{6,}\z/
17
+ SSH_PORT_RE = /ssh -p (\d+)/
18
+ private_constant :ID_RE, :SSH_PORT_RE
19
+
20
+ # The machine's Docker-style short id.
21
+ # @return [String]
22
+ attr_reader :id
23
+
24
+ # Host port forwarded to the guest's SSH, if the boot banner reported one.
25
+ # @return [Integer, nil]
26
+ attr_reader :ssh_port
27
+
28
+ # @param id [String]
29
+ # @param ssh_port [Integer, nil]
30
+ def initialize(id, ssh_port: nil)
31
+ @id = id
32
+ @ssh_port = ssh_port
33
+ end
34
+
35
+ class << self
36
+ # Boot a new microVM and return a handle to it.
37
+ #
38
+ # Accepts create options as a keyword list or a Hash; discriminated on
39
+ # +:os+ ("linux", "freebsd", "netbsd", "firmware", "kernel").
40
+ #
41
+ # @param opts [Hash]
42
+ # @return [Sandbox]
43
+ # @raise [CommandFailed] if boot fails or no machine id is printed.
44
+ def create(opts = {}, **kwargs)
45
+ opts = normalize(opts.merge(kwargs))
46
+ args = Args.build_create_args(opts)
47
+ res = Process.run(args, log_level: opts.fetch(:log_level, 1))
48
+ if res.exit_code != 0
49
+ raise CommandFailed.new(
50
+ exit_code: res.exit_code, stdout: res.stdout, stderr: res.stderr,
51
+ command: "bsdkrun create"
52
+ )
53
+ end
54
+ # Detached runs print just the machine id on stdout.
55
+ id = res.stdout.split("\n").map(&:strip).select { |l| l.match?(ID_RE) }.last
56
+ unless id
57
+ raise CommandFailed.new(
58
+ exit_code: res.exit_code, stdout: res.stdout, stderr: res.stderr,
59
+ command: "bsdkrun create (no machine id in output)"
60
+ )
61
+ end
62
+ match = res.stderr.match(SSH_PORT_RE)
63
+ new(id, ssh_port: match && match[1].to_i)
64
+ end
65
+
66
+ # Reconnect to an existing machine by id (a unique prefix is enough).
67
+ #
68
+ # @param id [String]
69
+ # @return [Sandbox]
70
+ # @raise [SandboxNotFound]
71
+ def get(id)
72
+ row = list(all: true).find { |m| m.id == id || m.id.start_with?(id) }
73
+ raise SandboxNotFound, id unless row
74
+
75
+ new(row.id)
76
+ end
77
+
78
+ # List machines. +all: true+ includes exited ones (default running only).
79
+ #
80
+ # @param all [Boolean]
81
+ # @return [Array<SandboxInfo>]
82
+ def list(all: false)
83
+ args = ["ps", "--json"]
84
+ args << "--all" if all
85
+ res = Process.run!(args, label: "bsdkrun ps")
86
+ rows = JSON.parse(res.stdout.empty? ? "[]" : res.stdout)
87
+ rows.map { |row| SandboxInfo.from_row(row) }
88
+ end
89
+
90
+ # Symbolize keys and normalize a nested +:net+ hash.
91
+ # @!visibility private
92
+ def normalize(opts)
93
+ h = opts.each_with_object({}) { |(k, v), acc| acc[k.to_sym] = v }
94
+ h[:net] = h[:net].each_with_object({}) { |(k, v), acc| acc[k.to_sym] = v } if h[:net].is_a?(Hash)
95
+ h
96
+ end
97
+ end
98
+
99
+ # Run a command in the guest through its exec agent.
100
+ #
101
+ # +command+ may be an Array (argv, no shell parsing) or a String program
102
+ # name; with a String, +args:+ supplies its arguments.
103
+ #
104
+ # @param command [String, Array<String>]
105
+ # @param args [Array<String>] arguments when +command+ is a bare String.
106
+ # @param env [Hash] environment variables (+-e K=V+).
107
+ # @param tty [Boolean] allocate a pseudo-TTY in the guest (+-t+).
108
+ # @param stdin [String, nil] data piped to the command's stdin.
109
+ # @param cwd [String, nil] working directory (emulated via +sh -c 'cd …'+).
110
+ # @param throw_on_error [Boolean] raise {CommandFailed} on a non-zero exit.
111
+ # @param log_level [Integer] per-command bsdkrun log level.
112
+ # @return [Result]
113
+ def exec(command, args: [], env: {}, tty: false, stdin: nil, cwd: nil,
114
+ throw_on_error: false, log_level: 0)
115
+ argv = command.is_a?(Array) ? command.dup : [command, *args]
116
+
117
+ if cwd
118
+ # Emulate a working directory: cd, drop it, then exec the real argv.
119
+ argv = ["/bin/sh", "-c", 'cd "$1" && shift && exec "$@"', "sh", cwd, *argv]
120
+ end
121
+
122
+ cli = ["exec"]
123
+ cli << "-t" if tty
124
+ env.each { |k, v| cli.push("-e", "#{k}=#{v}") }
125
+ cli.push(@id, *argv)
126
+
127
+ res = Process.run(cli, stdin: stdin, log_level: log_level)
128
+ result = Result.new(
129
+ stdout: res.stdout, stderr: res.stderr, exit_code: res.exit_code,
130
+ command: "exec #{argv.join(' ')}"
131
+ )
132
+ result.throw_if_failed! if throw_on_error
133
+ result
134
+ end
135
+
136
+ # Vercel-Sandbox-style alias for {#exec}: a program plus its args.
137
+ #
138
+ # @param command [String]
139
+ # @param args [Array<String>]
140
+ # @return [Result]
141
+ def run_command(command, args = [], **opts)
142
+ exec(command, args: args, **opts)
143
+ end
144
+
145
+ # Read the machine's console log.
146
+ #
147
+ # @param boot [Boolean] show bsdkrun's own boot log instead of the console.
148
+ # @return [String]
149
+ def logs(boot: false)
150
+ args = ["logs"]
151
+ args << "--boot" if boot
152
+ args << @id
153
+ Process.run(args).stdout
154
+ end
155
+
156
+ # Attach an interactive shell to the machine (inherits the terminal).
157
+ # @return [Boolean] true if the shell exited zero.
158
+ def shell
159
+ Process.spawn_interactive(["shell", @id])
160
+ end
161
+
162
+ # Fetch this machine's current status row, or nil if it's gone.
163
+ # @return [SandboxInfo, nil]
164
+ def status
165
+ Sandbox.list(all: true).find { |m| m.id == @id }
166
+ end
167
+
168
+ # Whether the machine is currently running.
169
+ # @return [Boolean]
170
+ def running?
171
+ s = status
172
+ s ? s.running : false
173
+ end
174
+
175
+ # Stop the machine. BSD guests are cleanly powered off; Linux is SIGTERM'd.
176
+ # @return [void]
177
+ def stop
178
+ lifecycle(["stop", @id], "bsdkrun stop")
179
+ end
180
+
181
+ # Restart a stopped machine in place (same id, disk/rootfs). Boots detached.
182
+ # @return [void]
183
+ def start
184
+ lifecycle(["start", @id], "bsdkrun start")
185
+ end
186
+
187
+ # Remove the machine and its state. +force+ stops it first if running.
188
+ # @param force [Boolean]
189
+ # @return [void]
190
+ def remove(force: false)
191
+ args = ["rm"]
192
+ args << "--force" if force
193
+ args << @id
194
+ lifecycle(args, "bsdkrun rm")
195
+ end
196
+
197
+ # Change the recorded vCPU / RAM. Applies on the next {#start}.
198
+ # @param cpus [Integer, nil]
199
+ # @param mem [Integer, nil]
200
+ # @return [void]
201
+ def update(cpus: nil, mem: nil)
202
+ args = ["update", @id]
203
+ args.push("--cpus", cpus.to_s) unless cpus.nil?
204
+ args.push("--mem", mem.to_s) unless mem.nil?
205
+ lifecycle(args, "bsdkrun update")
206
+ end
207
+
208
+ # Join or switch this machine to a global network. Applies on next {#start}.
209
+ # @param network [String]
210
+ # @return [void]
211
+ def connect_network(network)
212
+ lifecycle(["network", "connect", @id, network], "bsdkrun network connect")
213
+ end
214
+
215
+ # Detach this machine from its network. Applies on next {#start}.
216
+ # @return [void]
217
+ def disconnect_network
218
+ lifecycle(["network", "disconnect", @id], "bsdkrun network disconnect")
219
+ end
220
+
221
+ # Install SSH keys in the guest via the agent (+ssh setup+). With no keys,
222
+ # the CLI installs your local +~/.ssh/*.pub+.
223
+ #
224
+ # @param user [String, nil] target user (default root).
225
+ # @param key [String, Array<String>, nil] literal key(s) or +.pub+ path(s).
226
+ # @return [Result]
227
+ def ssh_setup(user: nil, key: nil)
228
+ action = ["setup"]
229
+ action.push("--user", user) if user
230
+ Array(key).each { |k| action.push("--key", k) }
231
+ agent("ssh", action)
232
+ end
233
+
234
+ # Put the guest on your tailnet (+tailscale setup+).
235
+ #
236
+ # @param authkey [String, nil] tailnet auth key (sent as +TS_AUTHKEY+).
237
+ # @param hostname [String, nil] machine name on the tailnet.
238
+ # @param args [Array<String>] extra args passed through to +tailscale up+.
239
+ # @return [Result]
240
+ def tailscale_up(authkey: nil, hostname: nil, args: [])
241
+ action = ["setup"]
242
+ action.push("--hostname", hostname) if hostname
243
+ action.concat(args)
244
+ agent("tailscale", action, env: authkey ? { "TS_AUTHKEY" => authkey } : {})
245
+ end
246
+
247
+ private
248
+
249
+ # Run a fire-and-forget lifecycle CLI command, raising on failure.
250
+ def lifecycle(args, label)
251
+ Process.run!(args, label: label)
252
+ nil
253
+ end
254
+
255
+ # Run an in-guest agent CLI family (+ssh+, +tailscale+), raising on failure.
256
+ def agent(family, action, env: {})
257
+ res = Process.run([family, @id, *action], env: env)
258
+ result = Result.new(
259
+ stdout: res.stdout, stderr: res.stderr, exit_code: res.exit_code,
260
+ command: "#{family} #{action.join(' ')}"
261
+ )
262
+ result.throw_if_failed!
263
+ end
264
+ end
265
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bsdkrun
4
+ # Host-level toolchain / image operations.
5
+ module System
6
+ module_function
7
+
8
+ # Sanity-check the toolchain: verify libkrun links and a context can be
9
+ # created/configured. Does not boot.
10
+ # @return [Boolean] true on success.
11
+ def probe
12
+ Process.run(["probe"]).exit_code.zero?
13
+ end
14
+
15
+ # Download + prepare a BSD image ahead of time.
16
+ #
17
+ # @param os [String] "freebsd" or "netbsd".
18
+ # @param version [String, nil]
19
+ # @param force [Boolean] re-download even if cached.
20
+ # @return [String] the command output.
21
+ def fetch_image(os, version: nil, force: false)
22
+ args = ["fetch", "--os", os.to_s]
23
+ args.push("--version", version.to_s) if version
24
+ args << "--force" if force
25
+ Process.run!(args, label: "bsdkrun fetch").stdout
26
+ end
27
+
28
+ # List the builds available to fetch for a BSD, one version per line.
29
+ #
30
+ # @param os [String] "freebsd" or "netbsd".
31
+ # @return [Array<String>] version strings (non-empty output lines).
32
+ def versions(os)
33
+ res = Process.run!(["versions", "--os", os.to_s], label: "bsdkrun versions")
34
+ res.stdout.split("\n").map(&:strip).reject(&:empty?)
35
+ end
36
+
37
+ # Grow a raw disk image (the guest expands its root FS on next boot).
38
+ #
39
+ # @param disk [String]
40
+ # @param size [String]
41
+ # @return [void]
42
+ def grow_disk(disk, size)
43
+ Process.run!(["grow", "--disk", disk, "--size", size], label: "bsdkrun grow")
44
+ nil
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bsdkrun
4
+ # A machine as reported by +bsdkrun ps --json+.
5
+ #
6
+ # @!attribute [r] id
7
+ # @return [String]
8
+ # @!attribute [r] name
9
+ # @return [String, nil] DNS name on a network, or nil if unnamed.
10
+ # @!attribute [r] status
11
+ # @return [String] "running" or "exited" (derived from +running+).
12
+ SandboxInfo = Data.define(
13
+ :id, :name, :image, :kind, :command, :status, :running, :exit_code,
14
+ :pid, :detached, :cpus, :mem, :volume, :state_dir, :network, :net_ip,
15
+ :created_at, :finished_at
16
+ ) do
17
+ # Map a +ps --json+ row (String keys) to a typed instance.
18
+ # @param row [Hash]
19
+ # @return [SandboxInfo]
20
+ def self.from_row(row)
21
+ running = !!row["running"]
22
+ new(
23
+ id: row["id"].to_s,
24
+ name: row["name"],
25
+ image: row["image"].to_s,
26
+ kind: row["kind"].to_s,
27
+ command: (row["command"] || "").to_s,
28
+ status: running ? "running" : "exited",
29
+ running: running,
30
+ exit_code: to_i_or_nil(row["exit_code"]),
31
+ pid: to_i_or_nil(row["pid"]),
32
+ detached: !!row["detached"],
33
+ cpus: row["cpus"].to_i,
34
+ mem: row["mem"].to_i,
35
+ volume: row["volume"],
36
+ state_dir: row["state_dir"].to_s,
37
+ network: row["network"],
38
+ net_ip: row["net_ip"],
39
+ created_at: row["created_at"].to_i,
40
+ finished_at: to_i_or_nil(row["finished_at"])
41
+ )
42
+ end
43
+
44
+ def self.to_i_or_nil(value)
45
+ value.nil? ? nil : value.to_i
46
+ end
47
+ end
48
+
49
+ # An image as reported by +bsdkrun images --json+.
50
+ ImageInfo = Data.define(:id, :reference, :digest, :size, :rootfs, :created_at) do
51
+ # @param row [Hash]
52
+ # @return [ImageInfo]
53
+ def self.from_row(row)
54
+ new(
55
+ id: row["id"].to_s,
56
+ reference: row["reference"].to_s,
57
+ digest: row["digest"].to_s,
58
+ size: row["size"].to_i,
59
+ rootfs: row["rootfs"].to_s,
60
+ created_at: row["created_at"].to_i
61
+ )
62
+ end
63
+ end
64
+
65
+ # A volume as reported by +bsdkrun volume ls --json+.
66
+ VolumeInfo = Data.define(:name, :guest, :base, :path, :size, :created_at, :tracked) do
67
+ # @param row [Hash]
68
+ # @return [VolumeInfo]
69
+ def self.from_row(row)
70
+ created = row["created_at"]
71
+ new(
72
+ name: row["name"].to_s,
73
+ guest: row["guest"],
74
+ base: row["base"],
75
+ path: row["path"].to_s,
76
+ size: row["size"].to_s,
77
+ created_at: created.nil? ? nil : created.to_i,
78
+ tracked: !!row["tracked"]
79
+ )
80
+ end
81
+ end
82
+
83
+ # A global network as reported by +bsdkrun network ls --json+.
84
+ NetworkInfo = Data.define(:name, :subnet, :gateway, :members, :running, :up, :created_at) do
85
+ # @param row [Hash]
86
+ # @return [NetworkInfo]
87
+ def self.from_row(row)
88
+ created = row["created_at"]
89
+ new(
90
+ name: row["name"].to_s,
91
+ subnet: row["subnet"].to_s,
92
+ gateway: row["gateway"].to_s,
93
+ members: (row["members"] || 0).to_i,
94
+ running: (row["running"] || 0).to_i,
95
+ up: !!row["up"],
96
+ created_at: created.nil? ? nil : created.to_i
97
+ )
98
+ end
99
+ end
100
+
101
+ # The captured result of running a command in a guest via {Sandbox#exec}.
102
+ #
103
+ # @!attribute [r] stdout
104
+ # @return [String]
105
+ # @!attribute [r] stderr
106
+ # @return [String]
107
+ # @!attribute [r] exit_code
108
+ # @return [Integer]
109
+ # @!attribute [r] command
110
+ # @return [String] a human label for the command.
111
+ Result = Data.define(:stdout, :stderr, :exit_code, :command) do
112
+ # @return [Boolean] whether the command succeeded (exit 0).
113
+ def ok?
114
+ exit_code.zero?
115
+ end
116
+
117
+ # @return [String] stdout with trailing newlines trimmed — the common case.
118
+ def text
119
+ stdout.sub(/\n+\z/, "")
120
+ end
121
+
122
+ # @return [Object] stdout parsed as JSON.
123
+ def json
124
+ require "json"
125
+ JSON.parse(stdout)
126
+ end
127
+
128
+ # @return [Array<String>] non-empty stdout lines.
129
+ def lines
130
+ stdout.split("\n").reject(&:empty?)
131
+ end
132
+
133
+ # @raise [CommandFailed] if the command exited non-zero.
134
+ # @return [self]
135
+ def throw_if_failed!
136
+ unless ok?
137
+ raise CommandFailed.new(
138
+ exit_code: exit_code, stdout: stdout, stderr: stderr, command: command
139
+ )
140
+ end
141
+ self
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bsdkrun
4
+ # The bsdkrun Ruby SDK version.
5
+ VERSION = "0.1.0"
6
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Bsdkrun
6
+ # Host-level volume operations.
7
+ module Volumes
8
+ module_function
9
+
10
+ # List persistent volumes.
11
+ # @return [Array<VolumeInfo>]
12
+ def list
13
+ res = Process.run!(["volume", "ls", "--json"], label: "bsdkrun volume ls")
14
+ rows = JSON.parse(res.stdout.empty? ? "[]" : res.stdout)
15
+ rows.map { |row| VolumeInfo.from_row(row) }
16
+ end
17
+
18
+ # Remove one or more volumes (and their data).
19
+ #
20
+ # @param names [String, Array<String>]
21
+ # @param force [Boolean]
22
+ # @return [void]
23
+ def remove(names, force: false)
24
+ args = ["volume", "rm"]
25
+ args << "--force" if force
26
+ args.concat(Array(names))
27
+ Process.run!(args, label: "bsdkrun volume rm")
28
+ nil
29
+ end
30
+ end
31
+ end
data/lib/bsdkrun.rb ADDED
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "bsdkrun/version"
4
+ require_relative "bsdkrun/errors"
5
+ require_relative "bsdkrun/binary"
6
+ require_relative "bsdkrun/process"
7
+ require_relative "bsdkrun/args"
8
+ require_relative "bsdkrun/types"
9
+ require_relative "bsdkrun/sandbox"
10
+ require_relative "bsdkrun/images"
11
+ require_relative "bsdkrun/volumes"
12
+ require_relative "bsdkrun/networks"
13
+ require_relative "bsdkrun/system"
14
+
15
+ # bsdkrun — a Ruby SDK for {https://github.com/tsirysndr/bsdkrun bsdkrun}, a
16
+ # Firecracker-style microVM launcher for BSD and Linux guests. A thin wrapper
17
+ # around the +bsdkrun+ CLI: it builds argv, shells out, and parses JSON output.
18
+ #
19
+ # @example
20
+ # require "bsdkrun"
21
+ #
22
+ # box = Bsdkrun::Sandbox.create(os: "linux", image: "alpine")
23
+ # puts box.exec(["uname", "-a"]).text
24
+ # box.stop
25
+ module Bsdkrun
26
+ class << self
27
+ # Force the SDK to use a specific +bsdkrun+ binary, bypassing discovery.
28
+ # @param path [String, nil]
29
+ # @return [void]
30
+ def binary_path=(path)
31
+ Binary.override = path
32
+ end
33
+
34
+ # @return [String] the resolved +bsdkrun+ binary path.
35
+ # @raise [BinaryNotFound]
36
+ def binary_path
37
+ Binary.resolve
38
+ end
39
+
40
+ # Reset cached binary discovery state (mainly for tests).
41
+ # @return [void]
42
+ def reset_binary!
43
+ Binary.reset!
44
+ end
45
+
46
+ # Convenience alias for {Sandbox.create}.
47
+ # @return [Sandbox]
48
+ def create(opts = {}, **kwargs)
49
+ Sandbox.create(opts, **kwargs)
50
+ end
51
+
52
+ # Convenience alias for {Sandbox.get}.
53
+ # @return [Sandbox]
54
+ def get(id)
55
+ Sandbox.get(id)
56
+ end
57
+
58
+ # Convenience alias for {Sandbox.list}.
59
+ # @return [Array<SandboxInfo>]
60
+ def list(all: false)
61
+ Sandbox.list(all: all)
62
+ end
63
+
64
+ # The {Images} namespace.
65
+ # @return [Module]
66
+ def images
67
+ Images
68
+ end
69
+
70
+ # The {Volumes} namespace.
71
+ # @return [Module]
72
+ def volumes
73
+ Volumes
74
+ end
75
+
76
+ # The {Networks} namespace.
77
+ # @return [Module]
78
+ def networks
79
+ Networks
80
+ end
81
+
82
+ # The {System} namespace.
83
+ # @return [Module]
84
+ def system
85
+ System
86
+ end
87
+ end
88
+ end
metadata ADDED
@@ -0,0 +1,88 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: bsdkrun
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Tsiry Sandratraina
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: minitest
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '5.0'
19
+ type: :development
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '5.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: rake
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '13.0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '13.0'
40
+ description: |
41
+ A thin, dependency-free Ruby wrapper around the `bsdkrun` CLI. Boot and drive
42
+ BSD/Linux microVMs programmatically: create sandboxes, exec commands, manage
43
+ lifecycle, and wire up global networks, volumes and images.
44
+ email:
45
+ - tsiry.sndr@gmail.com
46
+ executables: []
47
+ extensions: []
48
+ extra_rdoc_files: []
49
+ files:
50
+ - README.md
51
+ - lib/bsdkrun.rb
52
+ - lib/bsdkrun/args.rb
53
+ - lib/bsdkrun/binary.rb
54
+ - lib/bsdkrun/errors.rb
55
+ - lib/bsdkrun/images.rb
56
+ - lib/bsdkrun/networks.rb
57
+ - lib/bsdkrun/process.rb
58
+ - lib/bsdkrun/sandbox.rb
59
+ - lib/bsdkrun/system.rb
60
+ - lib/bsdkrun/types.rb
61
+ - lib/bsdkrun/version.rb
62
+ - lib/bsdkrun/volumes.rb
63
+ homepage: https://github.com/tsirysndr/bsdkrun
64
+ licenses:
65
+ - MIT
66
+ metadata:
67
+ homepage_uri: https://github.com/tsirysndr/bsdkrun
68
+ source_code_uri: https://github.com/tsirysndr/bsdkrun/tree/main/sdk/ruby
69
+ rubygems_mfa_required: 'true'
70
+ rdoc_options: []
71
+ require_paths:
72
+ - lib
73
+ required_ruby_version: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ version: '3.2'
78
+ required_rubygems_version: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ requirements: []
84
+ rubygems_version: 4.0.10
85
+ specification_version: 4
86
+ summary: Ruby SDK for bsdkrun — a Firecracker-style microVM launcher for BSD and Linux
87
+ guests.
88
+ test_files: []