microsandbox-rb 0.14.0 → 0.16.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 +4 -4
- data/CHANGELOG.md +131 -0
- data/Cargo.lock +203 -82
- data/DESIGN.md +1 -1
- data/README.md +40 -3
- data/ext/microsandbox/Cargo.toml +5 -5
- data/ext/microsandbox/src/error.rs +5 -0
- data/ext/microsandbox/src/sandbox.rs +197 -18
- data/lib/microsandbox/errors.rb +7 -0
- data/lib/microsandbox/sandbox.rb +296 -2
- data/lib/microsandbox/ssh.rb +15 -2
- data/lib/microsandbox/version.rb +2 -2
- data/sig/microsandbox.rbs +19 -2
- metadata +1 -1
data/lib/microsandbox/sandbox.rb
CHANGED
|
@@ -20,6 +20,14 @@ module Microsandbox
|
|
|
20
20
|
# @return [String]
|
|
21
21
|
def name = @native.name
|
|
22
22
|
|
|
23
|
+
# The opaque, backend-assigned identity of the persisted sandbox this handle
|
|
24
|
+
# is bound to (runtime v0.6.16). Unlike {#name} — a reusable label — it is
|
|
25
|
+
# stable for the sandbox's lifetime and changes once the same name is
|
|
26
|
+
# removed and recreated; the convergent operations below compare against it
|
|
27
|
+
# before acting.
|
|
28
|
+
# @return [String]
|
|
29
|
+
def id = @native.id
|
|
30
|
+
|
|
23
31
|
# @return [Symbol] :created, :starting, :running, :draining, :paused,
|
|
24
32
|
# :stopped, or :crashed (a snapshot, captured when this handle was fetched)
|
|
25
33
|
def status = @native.status.to_sym
|
|
@@ -95,6 +103,68 @@ module Microsandbox
|
|
|
95
103
|
nil
|
|
96
104
|
end
|
|
97
105
|
|
|
106
|
+
# Converge on a live sandbox for this handle (runtime v0.6.16): connect
|
|
107
|
+
# when it is already running, wait while it is starting, or start it when it
|
|
108
|
+
# is created/stopped/crashed. A draining or paused sandbox is rejected
|
|
109
|
+
# rather than raced.
|
|
110
|
+
# @param detached [Boolean] applies only when a start is actually required
|
|
111
|
+
# @raise [SandboxReplacedError] if the name now refers to a different sandbox
|
|
112
|
+
# @return [Sandbox] the live sandbox
|
|
113
|
+
def connect_or_start(detached: false)
|
|
114
|
+
# May boot a microVM (the start arm), so it needs a provisioned runtime
|
|
115
|
+
# just like {Sandbox.create}/{Sandbox.start}. Memoized after the first
|
|
116
|
+
# call, so the extra check costs nothing on the connect arm.
|
|
117
|
+
Microsandbox.ensure_runtime!
|
|
118
|
+
opts = detached ? {"detached" => true} : {}
|
|
119
|
+
Sandbox.new(@native.connect_or_start(opts))
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Block until *this exact* sandbox reaches +status+ (runtime v0.6.16).
|
|
123
|
+
#
|
|
124
|
+
# The wait is uninterruptible from Ruby: the native call blocks with the GVL
|
|
125
|
+
# released and no unblock function, so Ctrl-C (and any other signal) only
|
|
126
|
+
# lands once it returns. There is no built-in timeout either — the core
|
|
127
|
+
# polls every 100ms forever, by design, leaving the deadline to the caller.
|
|
128
|
+
# So wait only for a state this *backend* can actually reach (`:created` and
|
|
129
|
+
# `:paused` are cloud-side states, unreachable locally), and if you need a
|
|
130
|
+
# deadline, run this on a Thread of its own and enforce the timeout there
|
|
131
|
+
# (`Timeout.timeout` around it would not fire).
|
|
132
|
+
# @param status [Symbol, String] :created, :starting, :running, :draining,
|
|
133
|
+
# :paused, :stopped, or :crashed
|
|
134
|
+
# @raise [ArgumentError] on an unknown status name
|
|
135
|
+
# @raise [SandboxReplacedError] if the name now refers to a different sandbox
|
|
136
|
+
# @return [SandboxHandle] a fresh handle observed in that state
|
|
137
|
+
def wait_for_status(status)
|
|
138
|
+
SandboxHandle.new(@native.wait_for_status(Sandbox.send(:coerce_status, status)))
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Stop and start *this exact* sandbox, returning the new live sandbox
|
|
142
|
+
# (runtime v0.6.16). A created/stopped/crashed sandbox is started directly.
|
|
143
|
+
# @param force [Boolean] SIGKILL instead of requesting a graceful shutdown
|
|
144
|
+
# @param timeout [Numeric, nil] graceful-shutdown seconds before escalating
|
|
145
|
+
# (the core's default is 10)
|
|
146
|
+
# @param detached [Boolean] start the replacement detached
|
|
147
|
+
# @raise [SandboxReplacedError] if the name now refers to a different sandbox
|
|
148
|
+
# @return [Sandbox]
|
|
149
|
+
def restart(force: false, timeout: nil, detached: false)
|
|
150
|
+
Microsandbox.ensure_runtime!
|
|
151
|
+
opts = Sandbox.send(:build_restart_opts, force: force, timeout: timeout, detached: detached)
|
|
152
|
+
Sandbox.new(@native.restart(opts))
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Stop and remove *this exact* sandbox (runtime v0.6.16) — {#stop} plus
|
|
156
|
+
# {Sandbox.remove} in one convergent step, with an identity check that
|
|
157
|
+
# refuses to remove a same-name replacement.
|
|
158
|
+
# @param force [Boolean] SIGKILL instead of requesting a graceful shutdown
|
|
159
|
+
# @param timeout [Numeric, nil] graceful-shutdown seconds before escalating
|
|
160
|
+
# (the core's default is 10)
|
|
161
|
+
# @raise [SandboxReplacedError] if the name now refers to a different sandbox
|
|
162
|
+
# @return [nil]
|
|
163
|
+
def destroy(force: false, timeout: nil)
|
|
164
|
+
@native.destroy(Sandbox.send(:build_destroy_opts, force: force, timeout: timeout))
|
|
165
|
+
nil
|
|
166
|
+
end
|
|
167
|
+
|
|
98
168
|
# Block until the sandbox is observed in a terminal (non-running) state.
|
|
99
169
|
# @return [SandboxStopResult]
|
|
100
170
|
def wait_until_stopped
|
|
@@ -265,6 +335,11 @@ module Microsandbox
|
|
|
265
335
|
# to gate the `fstype:`-vs-OCI check; keep in sync on a runtime-tag bump.
|
|
266
336
|
DISK_IMAGE_EXTENSIONS = %w[raw qcow2 vmdk].freeze
|
|
267
337
|
|
|
338
|
+
# The seven lifecycle states a sandbox can be observed in — the spelling
|
|
339
|
+
# {Sandbox#status} / {SandboxHandle#status} return, and the accepted
|
|
340
|
+
# arguments of {Sandbox#wait_for_status} / {SandboxHandle#wait_for_status}.
|
|
341
|
+
STATUSES = %i[created starting running draining paused stopped crashed].freeze
|
|
342
|
+
|
|
268
343
|
class << self
|
|
269
344
|
# Create and boot a sandbox.
|
|
270
345
|
#
|
|
@@ -309,7 +384,11 @@ module Microsandbox
|
|
|
309
384
|
# (4 GiB as of `v0.5.10`); the core rejects it on tmpfs/disk/named (for a
|
|
310
385
|
# named volume, set its quota via {Volume.create}). Bind/named mounts
|
|
311
386
|
# also accept `follow_root_symlinks: true` to opt out of the default-on
|
|
312
|
-
# mount-root symlink protection (runtime v0.6.7)
|
|
387
|
+
# mount-root symlink protection (runtime v0.6.7), and `uid:`/`gid:`
|
|
388
|
+
# (runtime v0.6.15) to pin the guest owner presented for host files that
|
|
389
|
+
# carry no per-file stat override — both must be given together, each an
|
|
390
|
+
# Integer in 0..4294967295, and they conflict with
|
|
391
|
+
# `stat_virtualization: :off`.
|
|
313
392
|
# @param network [Array, String, Symbol, NetworkPolicy, Hash, nil] network
|
|
314
393
|
# policy. Composable profiles (`[:public]` (the default), `[:public,
|
|
315
394
|
# :private]`, `:host`, …), a terminal preset (:none, :allow_all), a
|
|
@@ -414,6 +493,62 @@ module Microsandbox
|
|
|
414
493
|
end
|
|
415
494
|
end
|
|
416
495
|
|
|
496
|
+
# Converge on a live sandbox called +name+ (runtime v0.6.16, upstream
|
|
497
|
+
# #1462): connect to the persisted sandbox with that name — starting it
|
|
498
|
+
# when it is created/stopped/crashed, waiting when it is already starting
|
|
499
|
+
# — or create it when none exists. Concurrent callers converge on the
|
|
500
|
+
# winning identity instead of one of them losing to a name clash.
|
|
501
|
+
#
|
|
502
|
+
# The keyword options are exactly {create}'s and are used **only when a
|
|
503
|
+
# create actually happens**: an existing sandbox keeps its persisted
|
|
504
|
+
# configuration, so passing `memory:` here will not resize one that is
|
|
505
|
+
# already there. `replace:`/`replace_with_timeout:` are accepted for
|
|
506
|
+
# kwargs parity with {create} but are rejected by the core — replacing a
|
|
507
|
+
# sandbox is the opposite of converging on it — and raise
|
|
508
|
+
# {InvalidConfigError}.
|
|
509
|
+
#
|
|
510
|
+
# The block form yields the sandbox and then stops it **only when this
|
|
511
|
+
# call owns its lifecycle** ({Sandbox#owns_lifecycle?}). That is a
|
|
512
|
+
# deliberate divergence from {create}, whose block form always stops:
|
|
513
|
+
# `connect_or_create` may hand back a sandbox this process did not start,
|
|
514
|
+
# and tearing down someone else's long-lived service on the way out of a
|
|
515
|
+
# block is never what the caller meant. Concretely:
|
|
516
|
+
#
|
|
517
|
+
# - created here (the common case) — yielded, then stopped, exactly like
|
|
518
|
+
# {create};
|
|
519
|
+
# - *connected* to a sandbox that was already running — left running;
|
|
520
|
+
# - created here with `detached: true` — left running, which is what
|
|
521
|
+
# `detached:` asks for (this is where the divergence from {create} bites:
|
|
522
|
+
# `create(..., detached: true) { }` still stops on block exit).
|
|
523
|
+
#
|
|
524
|
+
# Either way, stop it yourself with {Sandbox#stop}/{Sandbox#destroy} when
|
|
525
|
+
# you do want it gone.
|
|
526
|
+
#
|
|
527
|
+
# The teardown is safe against name reuse from either side: it skips the
|
|
528
|
+
# stop entirely for a sandbox this call did not start, and the stop it does
|
|
529
|
+
# issue is scoped to this sandbox's {Sandbox#id} (runtime v0.6.16), so a
|
|
530
|
+
# name removed and recreated while the block ran raises inside the ensure
|
|
531
|
+
# — swallowed as a best-effort teardown failure — rather than taking the
|
|
532
|
+
# replacement down.
|
|
533
|
+
# @param name [String]
|
|
534
|
+
# @yieldparam sandbox [Sandbox]
|
|
535
|
+
# @return [Sandbox, Object] the live sandbox, or the block's return value
|
|
536
|
+
def connect_or_create(name, **kwargs, &block)
|
|
537
|
+
opts = build_create_opts(**kwargs)
|
|
538
|
+
sandbox = new(Native::Sandbox.connect_or_create(name.to_s, opts))
|
|
539
|
+
return sandbox unless block_given?
|
|
540
|
+
|
|
541
|
+
begin
|
|
542
|
+
yield sandbox
|
|
543
|
+
ensure
|
|
544
|
+
begin
|
|
545
|
+
sandbox.stop if sandbox.owns_lifecycle?
|
|
546
|
+
rescue Microsandbox::Error
|
|
547
|
+
# best-effort cleanup; ignore stop failures during teardown
|
|
548
|
+
end
|
|
549
|
+
end
|
|
550
|
+
end
|
|
551
|
+
|
|
417
552
|
# Create a sandbox while streaming image-pull progress. Accepts the same
|
|
418
553
|
# options as {create}; returns a {PullSession} — iterate it (an
|
|
419
554
|
# {Enumerable} of progress-event Hashes, each with a "kind"), then call
|
|
@@ -590,6 +725,40 @@ module Microsandbox
|
|
|
590
725
|
seconds
|
|
591
726
|
end
|
|
592
727
|
|
|
728
|
+
# Validate a `wait_for_status` target and lower it to the wire name.
|
|
729
|
+
# Accepts a Symbol or String in the {STATUSES} spelling; anything else is
|
|
730
|
+
# an ArgumentError (an unreachable typo would otherwise block forever —
|
|
731
|
+
# `wait_for_status` has no built-in timeout).
|
|
732
|
+
def coerce_status(status)
|
|
733
|
+
name = status.to_s
|
|
734
|
+
unless STATUSES.include?(name.to_sym)
|
|
735
|
+
raise ArgumentError,
|
|
736
|
+
"unknown sandbox status #{status.inspect} " \
|
|
737
|
+
"(expected one of: #{STATUSES.join(", ")})"
|
|
738
|
+
end
|
|
739
|
+
name
|
|
740
|
+
end
|
|
741
|
+
|
|
742
|
+
# Shared option builder for `restart`, on both {Sandbox} and
|
|
743
|
+
# {SandboxHandle}. An omitted `timeout:` keeps the core's ten-second
|
|
744
|
+
# graceful-shutdown default.
|
|
745
|
+
def build_restart_opts(force:, timeout:, detached:)
|
|
746
|
+
opts = {}
|
|
747
|
+
opts["force"] = true if force
|
|
748
|
+
opts["detached"] = true if detached
|
|
749
|
+
opts["timeout"] = coerce_duration(timeout, "timeout") if timeout
|
|
750
|
+
opts
|
|
751
|
+
end
|
|
752
|
+
|
|
753
|
+
# Shared option builder for `destroy`, on both {Sandbox} and
|
|
754
|
+
# {SandboxHandle}.
|
|
755
|
+
def build_destroy_opts(force:, timeout:)
|
|
756
|
+
opts = {}
|
|
757
|
+
opts["force"] = true if force
|
|
758
|
+
opts["timeout"] = coerce_duration(timeout, "timeout") if timeout
|
|
759
|
+
opts
|
|
760
|
+
end
|
|
761
|
+
|
|
593
762
|
def stringify(hash)
|
|
594
763
|
hash.each_with_object({}) { |(k, v), acc| acc[k.to_s] = v.to_s }
|
|
595
764
|
end
|
|
@@ -987,7 +1156,8 @@ module Microsandbox
|
|
|
987
1156
|
# { tmpfs: true, size_mib: 64 } # memory-backed
|
|
988
1157
|
# { disk: "/img.raw", format: "raw", fstype: "ext4" } # disk-image mount
|
|
989
1158
|
# Any mount may also carry stat_virtualization: (:strict/:relaxed/:off) and
|
|
990
|
-
# host_permissions: (:private/:mirror); a bind mount may carry
|
|
1159
|
+
# host_permissions: (:private/:mirror); a bind/named mount may carry
|
|
1160
|
+
# uid:/gid: (the fallback guest owner); a bind mount may carry quota_mib:.
|
|
991
1161
|
# Coerce the `root_disk:` argument — an Integer (managed size in MiB), a
|
|
992
1162
|
# {RootDisk} factory Hash, or an equivalent hand-written Hash — into the
|
|
993
1163
|
# wire shape, validating kind/field combinations up front (mirrors the
|
|
@@ -1115,6 +1285,8 @@ module Microsandbox
|
|
|
1115
1285
|
# `follow_root_symlinks:` opts out of the default-on mount-root symlink
|
|
1116
1286
|
# protection (runtime v0.6.7; bind/named only — the core silently ignores
|
|
1117
1287
|
# it on tmpfs/disk mounts, so reject those here instead).
|
|
1288
|
+
# `uid:`/`gid:` pin the fallback guest owner (runtime v0.6.15) — see
|
|
1289
|
+
# {apply_mount_owner}.
|
|
1118
1290
|
def apply_mount_flags(mount, spec)
|
|
1119
1291
|
mount["readonly"] = true if spec[:ro] || spec["ro"] || spec[:readonly] || spec["readonly"]
|
|
1120
1292
|
mount["noexec"] = true if spec[:noexec] || spec["noexec"]
|
|
@@ -1134,6 +1306,58 @@ module Microsandbox
|
|
|
1134
1306
|
end
|
|
1135
1307
|
mount["follow_root_symlinks"] = !!follow
|
|
1136
1308
|
end
|
|
1309
|
+
apply_mount_owner(mount, spec)
|
|
1310
|
+
end
|
|
1311
|
+
|
|
1312
|
+
# Apply a volume spec Hash's fallback mount ownership (runtime v0.6.15,
|
|
1313
|
+
# upstream #1451). Host files carrying no per-file stat override surface in
|
|
1314
|
+
# the guest as `uid:`/`gid:` instead of the runtime's fallback owner; the
|
|
1315
|
+
# pair travels on the wire as `override_uid`/`override_gid` (the core's
|
|
1316
|
+
# `MountBuilder#owner`), mirroring the Python SDK's concise `uid:`/`gid:`
|
|
1317
|
+
# public names over the same wire fields.
|
|
1318
|
+
#
|
|
1319
|
+
# Validation mirrors the Python SDK exactly: the two must be given
|
|
1320
|
+
# together, each must be a plain Integer in the u32 range, they need stat
|
|
1321
|
+
# virtualization (so `stat_virtualization: :off` conflicts), and they only
|
|
1322
|
+
# apply to bind/named mounts. The one Python rule with no Ruby counterpart
|
|
1323
|
+
# is "not supported for disk-backed named volumes": Python knows the
|
|
1324
|
+
# volume kind because its `Volume.named` carries it, while a Ruby
|
|
1325
|
+
# `{ named: "vol" }` spec only references an existing volume by name — the
|
|
1326
|
+
# core rejects that combination at create() instead.
|
|
1327
|
+
def apply_mount_owner(mount, spec)
|
|
1328
|
+
uid = spec.fetch(:uid, spec["uid"])
|
|
1329
|
+
gid = spec.fetch(:gid, spec["gid"])
|
|
1330
|
+
return if uid.nil? && gid.nil?
|
|
1331
|
+
|
|
1332
|
+
if uid.nil? || gid.nil?
|
|
1333
|
+
raise ArgumentError, "mount uid: and gid: must be set together"
|
|
1334
|
+
end
|
|
1335
|
+
unless %w[bind named].include?(mount["kind"])
|
|
1336
|
+
raise ArgumentError,
|
|
1337
|
+
"uid:/gid: (mount owner) only applies to bind/named mounts " \
|
|
1338
|
+
"(got a #{mount["kind"]} mount), like stat_virtualization:/host_permissions:"
|
|
1339
|
+
end
|
|
1340
|
+
if mount["stat_virtualization"] == "off"
|
|
1341
|
+
raise ArgumentError,
|
|
1342
|
+
"uid:/gid: (mount owner) cannot be combined with stat_virtualization: :off — " \
|
|
1343
|
+
"`off` exposes literal host metadata, leaving no overlay to rewrite the owner in"
|
|
1344
|
+
end
|
|
1345
|
+
mount["override_uid"] = coerce_mount_owner_id(uid, "uid:")
|
|
1346
|
+
mount["override_gid"] = coerce_mount_owner_id(gid, "gid:")
|
|
1347
|
+
end
|
|
1348
|
+
|
|
1349
|
+
# Range-check one mount owner ID. Deliberately stricter than the repo's
|
|
1350
|
+
# usual `Integer(value)` coercion: `Integer("1000")` and a truncating
|
|
1351
|
+
# `Integer(1000.7)` would both silently accept input that never named a
|
|
1352
|
+
# real owner. `is_a?(Integer)` also rejects `true`/`false`, matching the
|
|
1353
|
+
# Python SDK's `_mount_owner_id` (`type(value) is not int`, which excludes
|
|
1354
|
+
# `bool`).
|
|
1355
|
+
def coerce_mount_owner_id(value, label)
|
|
1356
|
+
unless value.is_a?(Integer) && value >= 0 && value <= 0xFFFF_FFFF
|
|
1357
|
+
raise ArgumentError,
|
|
1358
|
+
"#{label} must be an Integer between 0 and 4294967295 (got #{value.inspect})"
|
|
1359
|
+
end
|
|
1360
|
+
value
|
|
1137
1361
|
end
|
|
1138
1362
|
|
|
1139
1363
|
# Translate the pre-0.7.0 `options:` array form (e.g. options: %w[ro noexec])
|
|
@@ -1201,6 +1425,16 @@ module Microsandbox
|
|
|
1201
1425
|
@native.name
|
|
1202
1426
|
end
|
|
1203
1427
|
|
|
1428
|
+
# The opaque, backend-assigned identity of the persisted sandbox (runtime
|
|
1429
|
+
# v0.6.16). Unlike {#name} — a reusable label — this is stable for the
|
|
1430
|
+
# sandbox's lifetime and changes once the same name is removed and
|
|
1431
|
+
# recreated, which is exactly what {#wait_for_status}/{#restart}/{#destroy}
|
|
1432
|
+
# check before acting.
|
|
1433
|
+
# @return [String]
|
|
1434
|
+
def id
|
|
1435
|
+
@native.id
|
|
1436
|
+
end
|
|
1437
|
+
|
|
1204
1438
|
# Run a command (no shell interpretation) and collect its output.
|
|
1205
1439
|
#
|
|
1206
1440
|
# @param command [String] the executable
|
|
@@ -1488,6 +1722,13 @@ module Microsandbox
|
|
|
1488
1722
|
# Gracefully stop the sandbox (SIGTERM→SIGKILL escalation, 10s default) and
|
|
1489
1723
|
# wait for it to terminate. For a custom timeout or fire-and-return
|
|
1490
1724
|
# `request_*` control, fetch a {SandboxHandle} via {Sandbox.get}.
|
|
1725
|
+
#
|
|
1726
|
+
# Scoped to *this exact* sandbox: as of runtime v0.6.16 the stop carries this
|
|
1727
|
+
# object's {#id}, so if the name has since been removed and recreated the
|
|
1728
|
+
# call raises {SandboxReplacedError} (or {SandboxNotFoundError} when nothing
|
|
1729
|
+
# holds the name any more) instead of terminating whatever sandbox now
|
|
1730
|
+
# answers to it. Same for {#kill}, {#drain} and {#stop_and_wait}.
|
|
1731
|
+
# @raise [SandboxReplacedError] if the name now refers to a different sandbox
|
|
1491
1732
|
# @return [nil]
|
|
1492
1733
|
def stop
|
|
1493
1734
|
@native.stop
|
|
@@ -1520,6 +1761,59 @@ module Microsandbox
|
|
|
1520
1761
|
ExitStatus.new(@native.wait)
|
|
1521
1762
|
end
|
|
1522
1763
|
|
|
1764
|
+
# Block until *this exact* sandbox reaches +status+ (runtime v0.6.16). If
|
|
1765
|
+
# the name has since been recreated as a different sandbox, this raises
|
|
1766
|
+
# {SandboxReplacedError} rather than silently redirecting the wait to the
|
|
1767
|
+
# replacement.
|
|
1768
|
+
#
|
|
1769
|
+
# The wait is uninterruptible from Ruby: the native call blocks with the GVL
|
|
1770
|
+
# released and no unblock function, so Ctrl-C (and any other signal) only
|
|
1771
|
+
# lands once it returns. There is no built-in timeout either — the core
|
|
1772
|
+
# polls every 100ms forever, by design, leaving the deadline to the caller.
|
|
1773
|
+
# So wait only for a state this *backend* can actually reach (`:created` and
|
|
1774
|
+
# `:paused` are cloud-side states, unreachable locally), and if you need a
|
|
1775
|
+
# deadline, run this on a Thread of its own and enforce the timeout there
|
|
1776
|
+
# (`Timeout.timeout` around it would not fire).
|
|
1777
|
+
# @param status [Symbol, String] :created, :starting, :running, :draining,
|
|
1778
|
+
# :paused, :stopped, or :crashed
|
|
1779
|
+
# @raise [ArgumentError] on an unknown status name
|
|
1780
|
+
# @return [SandboxHandle] a fresh handle observed in that state (mirrors the
|
|
1781
|
+
# official SDKs, which return a handle from both `Sandbox` and
|
|
1782
|
+
# `SandboxHandle`)
|
|
1783
|
+
def wait_for_status(status)
|
|
1784
|
+
SandboxHandle.new(@native.wait_for_status(self.class.send(:coerce_status, status)))
|
|
1785
|
+
end
|
|
1786
|
+
|
|
1787
|
+
# Stop and start *this exact* sandbox, returning the new live sandbox
|
|
1788
|
+
# (runtime v0.6.16). A created/stopped/crashed sandbox is started directly.
|
|
1789
|
+
# This handle is spent afterwards — use the returned one.
|
|
1790
|
+
# @param force [Boolean] SIGKILL instead of requesting a graceful shutdown
|
|
1791
|
+
# @param timeout [Numeric, nil] graceful-shutdown seconds before escalating
|
|
1792
|
+
# (the core's default is 10)
|
|
1793
|
+
# @param detached [Boolean] start the replacement detached
|
|
1794
|
+
# @raise [SandboxReplacedError] if the name now refers to a different sandbox
|
|
1795
|
+
# @return [Sandbox]
|
|
1796
|
+
def restart(force: false, timeout: nil, detached: false)
|
|
1797
|
+
# Boots a microVM again, so it needs a provisioned runtime — the sandbox
|
|
1798
|
+
# this object came from may have been created in a different process.
|
|
1799
|
+
Microsandbox.ensure_runtime!
|
|
1800
|
+
opts = self.class.send(:build_restart_opts, force: force, timeout: timeout, detached: detached)
|
|
1801
|
+
self.class.new(@native.restart(opts))
|
|
1802
|
+
end
|
|
1803
|
+
|
|
1804
|
+
# Stop and remove *this exact* sandbox (runtime v0.6.16) — the convergent
|
|
1805
|
+
# equivalent of {#stop} followed by {Sandbox.remove}, with an identity check
|
|
1806
|
+
# that refuses to remove a same-name replacement.
|
|
1807
|
+
# @param force [Boolean] SIGKILL instead of requesting a graceful shutdown
|
|
1808
|
+
# @param timeout [Numeric, nil] graceful-shutdown seconds before escalating
|
|
1809
|
+
# (the core's default is 10)
|
|
1810
|
+
# @raise [SandboxReplacedError] if the name now refers to a different sandbox
|
|
1811
|
+
# @return [nil]
|
|
1812
|
+
def destroy(force: false, timeout: nil)
|
|
1813
|
+
@native.destroy(self.class.send(:build_destroy_opts, force: force, timeout: timeout))
|
|
1814
|
+
nil
|
|
1815
|
+
end
|
|
1816
|
+
|
|
1523
1817
|
# The live status, fetched from the backend (a round-trip per call).
|
|
1524
1818
|
# @return [Symbol] :created, :starting, :running, :draining, :paused,
|
|
1525
1819
|
# :stopped, or :crashed
|
data/lib/microsandbox/ssh.rb
CHANGED
|
@@ -231,11 +231,17 @@ module Microsandbox
|
|
|
231
231
|
# @param user [String] guest user to authenticate as (default "root")
|
|
232
232
|
# @param term [String, nil] TERM value for the session
|
|
233
233
|
# @param sftp [Boolean] enable the SFTP subsystem (default true)
|
|
234
|
+
# @param inactivity_timeout [Numeric, nil] per-session inactivity timeout in
|
|
235
|
+
# seconds; `nil` inherits the global config (default 600s), `0` disables it
|
|
234
236
|
# @yieldparam client [SshClient]
|
|
235
237
|
# @return [SshClient, Object]
|
|
236
|
-
def open_client(user: "root", term: nil, sftp: true)
|
|
238
|
+
def open_client(user: "root", term: nil, sftp: true, inactivity_timeout: nil)
|
|
237
239
|
opts = {"user" => user.to_s, "sftp" => sftp ? true : false}
|
|
238
240
|
opts["term"] = term.to_s if term
|
|
241
|
+
unless inactivity_timeout.nil?
|
|
242
|
+
opts["inactivity_timeout"] =
|
|
243
|
+
Sandbox.send(:coerce_duration, inactivity_timeout, "inactivity_timeout")
|
|
244
|
+
end
|
|
239
245
|
client = SshClient.new(@native.ssh_open_client(opts))
|
|
240
246
|
return client unless block_given?
|
|
241
247
|
|
|
@@ -251,12 +257,19 @@ module Microsandbox
|
|
|
251
257
|
# @param authorized_keys_path [String, nil] authorized_keys file path
|
|
252
258
|
# @param user [String, nil] guest user connections run as
|
|
253
259
|
# @param sftp [Boolean] enable the SFTP subsystem (default true)
|
|
260
|
+
# @param inactivity_timeout [Numeric, nil] per-session inactivity timeout in
|
|
261
|
+
# seconds; `nil` inherits the global config (default 600s), `0` disables it
|
|
254
262
|
# @return [SshServer]
|
|
255
|
-
def prepare_server(host_key_path: nil, authorized_keys_path: nil, user: nil, sftp: true
|
|
263
|
+
def prepare_server(host_key_path: nil, authorized_keys_path: nil, user: nil, sftp: true,
|
|
264
|
+
inactivity_timeout: nil)
|
|
256
265
|
opts = {"sftp" => sftp ? true : false}
|
|
257
266
|
opts["host_key_path"] = host_key_path.to_s if host_key_path
|
|
258
267
|
opts["authorized_keys_path"] = authorized_keys_path.to_s if authorized_keys_path
|
|
259
268
|
opts["user"] = user.to_s if user
|
|
269
|
+
unless inactivity_timeout.nil?
|
|
270
|
+
opts["inactivity_timeout"] =
|
|
271
|
+
Sandbox.send(:coerce_duration, inactivity_timeout, "inactivity_timeout")
|
|
272
|
+
end
|
|
260
273
|
SshServer.new(@native.ssh_prepare_server(opts))
|
|
261
274
|
end
|
|
262
275
|
end
|
data/lib/microsandbox/version.rb
CHANGED
|
@@ -8,12 +8,12 @@ module Microsandbox
|
|
|
8
8
|
# Versioning section of the README for the full gem-to-runtime map. Must equal
|
|
9
9
|
# the native ext's Cargo crate version (`Native.version`), enforced by
|
|
10
10
|
# spec/unit/version_spec.rb.
|
|
11
|
-
VERSION = "0.
|
|
11
|
+
VERSION = "0.16.0"
|
|
12
12
|
|
|
13
13
|
# The upstream microsandbox runtime release this gem build embeds — the `tag`
|
|
14
14
|
# pinned on the `microsandbox`/`microsandbox-network` git deps in
|
|
15
15
|
# ext/microsandbox/Cargo.toml. Exposed at runtime as
|
|
16
16
|
# {Microsandbox.runtime_version}. spec/unit/version_spec.rb asserts it stays in
|
|
17
17
|
# sync with the Cargo tag so it can't silently drift out of date.
|
|
18
|
-
RUNTIME_VERSION = "v0.6.
|
|
18
|
+
RUNTIME_VERSION = "v0.6.16"
|
|
19
19
|
end
|
data/sig/microsandbox.rbs
CHANGED
|
@@ -39,6 +39,7 @@ module Microsandbox
|
|
|
39
39
|
class SandboxNotRunningError < Error end
|
|
40
40
|
class SandboxAlreadyExistsError < Error end
|
|
41
41
|
class SandboxStillRunningError < Error end
|
|
42
|
+
class SandboxReplacedError < Error end
|
|
42
43
|
class ExecTimeoutError < Error end
|
|
43
44
|
class ExecFailedError < Error end
|
|
44
45
|
class NoDefaultCommandError < Error end
|
|
@@ -163,6 +164,7 @@ module Microsandbox
|
|
|
163
164
|
|
|
164
165
|
class SandboxHandle
|
|
165
166
|
def name: () -> String
|
|
167
|
+
def id: () -> String
|
|
166
168
|
def status: () -> Symbol
|
|
167
169
|
def running?: () -> bool
|
|
168
170
|
def stopped?: () -> bool
|
|
@@ -176,6 +178,10 @@ module Microsandbox
|
|
|
176
178
|
def request_kill: () -> nil
|
|
177
179
|
def request_drain: () -> nil
|
|
178
180
|
def wait_until_stopped: () -> SandboxStopResult
|
|
181
|
+
def connect_or_start: (?detached: bool) -> Sandbox
|
|
182
|
+
def wait_for_status: (Symbol | String status) -> SandboxHandle
|
|
183
|
+
def restart: (?force: bool, ?timeout: Numeric?, ?detached: bool) -> Sandbox
|
|
184
|
+
def destroy: (?force: bool, ?timeout: Numeric?) -> nil
|
|
179
185
|
def config_json: () -> String
|
|
180
186
|
def config: () -> Hash[String, untyped]
|
|
181
187
|
def snapshot: (String name) -> SnapshotInfo
|
|
@@ -243,6 +249,8 @@ module Microsandbox
|
|
|
243
249
|
end
|
|
244
250
|
|
|
245
251
|
class Sandbox
|
|
252
|
+
STATUSES: Array[Symbol]
|
|
253
|
+
|
|
246
254
|
def self.create: (String name, ?image: String?, ?cpus: Integer?, ?max_cpus: Integer?,
|
|
247
255
|
?memory: Integer?, ?max_memory: Integer?,
|
|
248
256
|
?env: Hash[untyped, untyped]?, ?workdir: String?, ?shell: String?,
|
|
@@ -269,6 +277,10 @@ module Microsandbox
|
|
|
269
277
|
?detached: bool,
|
|
270
278
|
?replace: bool, ?replace_with_timeout: Numeric?)
|
|
271
279
|
?{ (Sandbox) -> untyped } -> untyped
|
|
280
|
+
# Accepts the same keyword options as {create} (used only when a create is
|
|
281
|
+
# actually necessary); `replace:`/`replace_with_timeout:` are rejected by
|
|
282
|
+
# the core.
|
|
283
|
+
def self.connect_or_create: (String name, **untyped) ?{ (Sandbox) -> untyped } -> untyped
|
|
272
284
|
# Accepts the same keyword options as {create}.
|
|
273
285
|
def self.create_with_progress: (String name, **untyped) -> PullSession
|
|
274
286
|
def self.start: (String name, ?detached: bool) -> Sandbox
|
|
@@ -278,6 +290,7 @@ module Microsandbox
|
|
|
278
290
|
def self.remove: (String name) -> nil
|
|
279
291
|
|
|
280
292
|
def name: () -> String
|
|
293
|
+
def id: () -> String
|
|
281
294
|
def exec: (String command, ?Array[String] args, ?cwd: String?, ?user: String?,
|
|
282
295
|
?env: Hash[untyped, untyped]?, ?timeout: Numeric?, ?tty: bool, ?stdin: (String | :null)?,
|
|
283
296
|
?rlimits: Hash[untyped, untyped]?) -> ExecOutput
|
|
@@ -322,6 +335,9 @@ module Microsandbox
|
|
|
322
335
|
def kill: () -> nil
|
|
323
336
|
def drain: () -> nil
|
|
324
337
|
def wait: () -> ExitStatus
|
|
338
|
+
def wait_for_status: (Symbol | String status) -> SandboxHandle
|
|
339
|
+
def restart: (?force: bool, ?timeout: Numeric?, ?detached: bool) -> Sandbox
|
|
340
|
+
def destroy: (?force: bool, ?timeout: Numeric?) -> nil
|
|
325
341
|
def status: () -> Symbol
|
|
326
342
|
def owns_lifecycle?: () -> bool
|
|
327
343
|
def detach: () -> nil
|
|
@@ -653,8 +669,9 @@ module Microsandbox
|
|
|
653
669
|
|
|
654
670
|
class SshOps
|
|
655
671
|
def initialize: (untyped native) -> void
|
|
656
|
-
def open_client: (?user: String, ?term: String?, ?sftp: bool
|
|
672
|
+
def open_client: (?user: String, ?term: String?, ?sftp: bool,
|
|
673
|
+
?inactivity_timeout: Numeric?) ?{ (SshClient) -> untyped } -> untyped
|
|
657
674
|
def prepare_server: (?host_key_path: String?, ?authorized_keys_path: String?,
|
|
658
|
-
?user: String?, ?sftp: bool) -> SshServer
|
|
675
|
+
?user: String?, ?sftp: bool, ?inactivity_timeout: Numeric?) -> SshServer
|
|
659
676
|
end
|
|
660
677
|
end
|