boxd 0.2.19.dev901__tar.gz → 0.2.20__tar.gz
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.
- {boxd-0.2.19.dev901/src/boxd.egg-info → boxd-0.2.20}/PKG-INFO +173 -35
- {boxd-0.2.19.dev901 → boxd-0.2.20}/README.md +171 -34
- {boxd-0.2.19.dev901 → boxd-0.2.20}/pyproject.toml +9 -1
- {boxd-0.2.19.dev901 → boxd-0.2.20}/src/boxd/__init__.py +18 -0
- {boxd-0.2.19.dev901 → boxd-0.2.20}/src/boxd/_credentials.py +110 -29
- boxd-0.2.20/src/boxd/_generated/api_pb2.py +668 -0
- {boxd-0.2.19.dev901 → boxd-0.2.20}/src/boxd/_generated/api_pb2_grpc.py +428 -1
- {boxd-0.2.19.dev901 → boxd-0.2.20}/src/boxd/_mappers.py +46 -1
- {boxd-0.2.19.dev901 → boxd-0.2.20}/src/boxd/_requests.py +120 -12
- {boxd-0.2.19.dev901 → boxd-0.2.20}/src/boxd/_transport.py +24 -3
- boxd-0.2.20/src/boxd/errors.py +324 -0
- {boxd-0.2.19.dev901 → boxd-0.2.20}/src/boxd/models.py +96 -13
- boxd-0.2.20/src/boxd/py.typed +0 -0
- boxd-0.2.20/src/boxd/resources/fs.py +434 -0
- {boxd-0.2.19.dev901 → boxd-0.2.20}/src/boxd/resources/machines.py +260 -55
- {boxd-0.2.19.dev901 → boxd-0.2.20}/src/boxd/resources/vars.py +4 -3
- {boxd-0.2.19.dev901 → boxd-0.2.20}/src/boxd/spec.py +40 -11
- {boxd-0.2.19.dev901 → boxd-0.2.20/src/boxd.egg-info}/PKG-INFO +173 -35
- {boxd-0.2.19.dev901 → boxd-0.2.20}/src/boxd.egg-info/SOURCES.txt +4 -0
- {boxd-0.2.19.dev901 → boxd-0.2.20}/src/boxd.egg-info/requires.txt +1 -0
- {boxd-0.2.19.dev901 → boxd-0.2.20}/tests/test_credentials.py +236 -15
- {boxd-0.2.19.dev901 → boxd-0.2.20}/tests/test_delete_self.py +12 -2
- boxd-0.2.20/tests/test_fs.py +551 -0
- {boxd-0.2.19.dev901 → boxd-0.2.20}/tests/test_mappers.py +17 -0
- {boxd-0.2.19.dev901 → boxd-0.2.20}/tests/test_namespaces.py +153 -1
- boxd-0.2.20/tests/test_python_versions.py +115 -0
- {boxd-0.2.19.dev901 → boxd-0.2.20}/tests/test_requests.py +25 -4
- {boxd-0.2.19.dev901 → boxd-0.2.20}/tests/test_spec.py +170 -2
- {boxd-0.2.19.dev901 → boxd-0.2.20}/tests/test_transport.py +101 -0
- boxd-0.2.19.dev901/src/boxd/_generated/api_pb2.py +0 -626
- boxd-0.2.19.dev901/src/boxd/errors.py +0 -97
- {boxd-0.2.19.dev901 → boxd-0.2.20}/LICENSE +0 -0
- {boxd-0.2.19.dev901 → boxd-0.2.20}/setup.cfg +0 -0
- {boxd-0.2.19.dev901 → boxd-0.2.20}/src/boxd/_client.py +0 -0
- {boxd-0.2.19.dev901 → boxd-0.2.20}/src/boxd/_generated/__init__.py +0 -0
- {boxd-0.2.19.dev901 → boxd-0.2.20}/src/boxd/_streaming.py +0 -0
- {boxd-0.2.19.dev901 → boxd-0.2.20}/src/boxd/_urls.py +0 -0
- {boxd-0.2.19.dev901 → boxd-0.2.20}/src/boxd/_version_check.py +0 -0
- {boxd-0.2.19.dev901 → boxd-0.2.20}/src/boxd/resources/__init__.py +0 -0
- {boxd-0.2.19.dev901 → boxd-0.2.20}/src/boxd/resources/account.py +0 -0
- {boxd-0.2.19.dev901 → boxd-0.2.20}/src/boxd/resources/credentials.py +0 -0
- {boxd-0.2.19.dev901 → boxd-0.2.20}/src/boxd/resources/disks.py +0 -0
- {boxd-0.2.19.dev901 → boxd-0.2.20}/src/boxd/resources/domains.py +0 -0
- {boxd-0.2.19.dev901 → boxd-0.2.20}/src/boxd/resources/orgs.py +0 -0
- {boxd-0.2.19.dev901 → boxd-0.2.20}/src/boxd/resources/snapshots.py +0 -0
- {boxd-0.2.19.dev901 → boxd-0.2.20}/src/boxd/resources/teams.py +0 -0
- {boxd-0.2.19.dev901 → boxd-0.2.20}/src/boxd.egg-info/dependency_links.txt +0 -0
- {boxd-0.2.19.dev901 → boxd-0.2.20}/src/boxd.egg-info/top_level.txt +0 -0
- {boxd-0.2.19.dev901 → boxd-0.2.20}/tests/test_streaming.py +0 -0
- {boxd-0.2.19.dev901 → boxd-0.2.20}/tests/test_urls.py +0 -0
- {boxd-0.2.19.dev901 → boxd-0.2.20}/tests/test_version_check.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: boxd
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.20
|
|
4
4
|
Summary: Python SDK for the boxd cloud VM platform
|
|
5
5
|
Author: Azin
|
|
6
6
|
License-Expression: MIT
|
|
@@ -23,6 +23,7 @@ License-File: LICENSE
|
|
|
23
23
|
Requires-Dist: grpcio>=1.80
|
|
24
24
|
Requires-Dist: protobuf>=6.31.1
|
|
25
25
|
Requires-Dist: pydantic>=2
|
|
26
|
+
Requires-Dist: typing-extensions>=4.12
|
|
26
27
|
Requires-Dist: httpx<1,>=0.27
|
|
27
28
|
Provides-Extra: dev
|
|
28
29
|
Requires-Dist: grpcio-tools<=1.80,>=1.60; extra == "dev"
|
|
@@ -118,6 +119,12 @@ The first of these that is present wins:
|
|
|
118
119
|
If your key is revoked mid-session, the SDK fails fast with
|
|
119
120
|
`AuthenticationError` rather than retrying.
|
|
120
121
|
|
|
122
|
+
`AuthenticationError` means only that no credential was found or boxd refused
|
|
123
|
+
it; its `.reason` says which (`"missing_credentials"`, `"invalid_api_key"`,
|
|
124
|
+
`"invalid_token"`). Failing to reach boxd is an `APIConnectionError`, an
|
|
125
|
+
account that can't use the API a `PermissionDeniedError`, and an error answer
|
|
126
|
+
from the key exchange an `APIStatusError` with its `.http_status`.
|
|
127
|
+
|
|
121
128
|
The exchange endpoint is **rate-limited per source IP** (30 requests / 60s). A
|
|
122
129
|
session token is valid for one hour, so a fleet of workers behind one NAT
|
|
123
130
|
should **mint one token and share it** (`Boxd(token=...)`) rather than each
|
|
@@ -141,6 +148,9 @@ One limit: inside a **shared** machine the automatic credential can manage the
|
|
|
141
148
|
organization's shared machines, but cannot read environment variables or
|
|
142
149
|
secrets, and cannot reach private machines. Pass an API key for those.
|
|
143
150
|
|
|
151
|
+
An **isolated** machine has no automatic credential: `Boxd()` raises
|
|
152
|
+
`AuthenticationError` there, so pass an API key.
|
|
153
|
+
|
|
144
154
|
## Sync and async
|
|
145
155
|
|
|
146
156
|
`Boxd` and `AsyncBoxd` are the same surface — same namespaces, same method
|
|
@@ -204,7 +214,7 @@ boxd.machines.create(
|
|
|
204
214
|
disk="100G",
|
|
205
215
|
auto_suspend_timeout=300, # seconds; 0 disables
|
|
206
216
|
auto_destroy_timeout=0,
|
|
207
|
-
ssh=True, #
|
|
217
|
+
ssh=True, # False: SSH off, nothing answers on its port
|
|
208
218
|
proxies=[ProxyEntry(name="api", port=3000)],
|
|
209
219
|
volumes=[VolumeMount(disk_id="d_...", mount_path="/data", read_only=False)],
|
|
210
220
|
)
|
|
@@ -238,6 +248,7 @@ boxd.machines.unshare(id)
|
|
|
238
248
|
boxd.machines.set_auto_suspend_timeout(id, 300) # seconds idle; 0 disables
|
|
239
249
|
boxd.machines.set_auto_hibernate_timeout(id, 0)
|
|
240
250
|
boxd.machines.set_idle_ignore_outbound(id, True) # only inbound connections keep it awake
|
|
251
|
+
boxd.machines.set_ssh(id, False) # nothing answers SSH; exec still works
|
|
241
252
|
boxd.machines.wait_until_ready(id)
|
|
242
253
|
boxd.machines.wait_until_ready(id, timeout=180.0, poll_interval=1.0) # seconds
|
|
243
254
|
boxd.machines.suggest_name()
|
|
@@ -246,6 +257,14 @@ boxd.machines.suggest_name()
|
|
|
246
257
|
A fork inherits the source's sizing for anything you leave unset, and is
|
|
247
258
|
private to you unless you pass `shared=True`.
|
|
248
259
|
|
|
260
|
+
With SSH off (`ssh=False` on create, or `set_ssh(id, False)`), nothing
|
|
261
|
+
answers SSH on the machine's port, on the public address or the org's
|
|
262
|
+
network, and SSH sessions already open to it are closed. The port stays
|
|
263
|
+
allocated, so turning SSH back on keeps the address. `exec`, `boxd connect`
|
|
264
|
+
and the console terminal don't use SSH and keep working. A fork or a restore
|
|
265
|
+
keeps its source's setting unless you pass `ssh`. `set_ssh` can't be called
|
|
266
|
+
from inside the machine itself.
|
|
267
|
+
|
|
249
268
|
`create` and `fork` return once the machine is scheduled, not once it is
|
|
250
269
|
usable. Call `wait_until_ready` before doing anything that depends on it
|
|
251
270
|
running — especially before forking it again.
|
|
@@ -265,7 +284,6 @@ engineering = (
|
|
|
265
284
|
.network("eng")
|
|
266
285
|
.label("team", "eng")
|
|
267
286
|
.env("NODE_ENV", "development")
|
|
268
|
-
.egress("github.com", "*.atlassian.net", "*.ubuntu.com")
|
|
269
287
|
.packages("apt-get update", "apt-get install -y nginx")
|
|
270
288
|
.with_(github(shared="eng-bot"), claude())
|
|
271
289
|
)
|
|
@@ -332,6 +350,14 @@ the org's secrets count. Only org owners and admins may list an org
|
|
|
332
350
|
shared machines, so a private machine can't list one. A name that resolves to
|
|
333
351
|
nothing, or that the create may not list, fails the create, naming it.
|
|
334
352
|
|
|
353
|
+
Only org owners and admins may set, move or delete an org `unattached` secret;
|
|
354
|
+
your own (`private-unattached`) are yours. From inside a machine (with its
|
|
355
|
+
in-VM credentials) a spec can't list an unattached secret, yours or the org's,
|
|
356
|
+
on any machine, and `update_spec` can't change that machine's own `egress` or
|
|
357
|
+
`secrets`; make those changes from outside the machine, with an API key or in
|
|
358
|
+
the console. Names a fork or a restore takes over from its source's spec are
|
|
359
|
+
not affected.
|
|
360
|
+
|
|
335
361
|
On the machine, the spec's `env` and its `secrets` win over your `private`
|
|
336
362
|
vars, which win over the org's `shared` vars (shared machines only), which win
|
|
337
363
|
over the org's `all` vars. A name can't be in both `env` and `secrets`.
|
|
@@ -366,6 +392,12 @@ with an org API key or from inside a machine, for someone who isn't a member,
|
|
|
366
392
|
and with a snapshot or disk the member couldn't use. Your own id is a plain
|
|
367
393
|
create. `assignTo` is never inherited: forks, snapshots and restores drop it.
|
|
368
394
|
|
|
395
|
+
The machine runs the spec you wrote as the member's own machine: its packages
|
|
396
|
+
as root, its env and its base, alongside the member's GitHub token, agent
|
|
397
|
+
logins and personal connections. Assign a spec only if the member would run it
|
|
398
|
+
themselves. The member sees who created it (`created_by`) and can read the
|
|
399
|
+
spec (`machine.spec`).
|
|
400
|
+
|
|
369
401
|
#### Integrations the machine needs
|
|
370
402
|
|
|
371
403
|
A spec's `connections` and `agents` are the machine's requirements. Reading the
|
|
@@ -398,10 +430,17 @@ can't connect (`spec.connections.<toolkit>: ...`), and the GitHub App
|
|
|
398
430
|
|
|
399
431
|
`packages` are shell commands the machine runs once, in order, after its first
|
|
400
432
|
boot: as root, with bash (sh on an image without it),
|
|
401
|
-
`DEBIAN_FRONTEND=noninteractive`, no stdin
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
433
|
+
`DEBIAN_FRONTEND=noninteractive`, no stdin, the env and secrets an exec
|
|
434
|
+
session on the machine gets, and a 30-minute timeout each. Their output is
|
|
435
|
+
appended to `/var/log/boxd/packages.log` in the machine, which only root can
|
|
436
|
+
read. A command is done when its shell exits, so one that leaves a process
|
|
437
|
+
running in the background (`my-server &`) doesn't hold up the next. The first
|
|
438
|
+
failure (a non-zero exit, a timeout) stops the rest; the machine stays usable,
|
|
439
|
+
and `update_spec(id, {}, retry_packages=True)` runs the failed command and the ones after it again (so does
|
|
440
|
+
Retry in the console's Packages section). To give up on a command instead, or
|
|
441
|
+
to replace a wrong one, remove it from the spec: a command that leaves the spec
|
|
442
|
+
before it finished is skipped, and the ones after it run. Reading the machine
|
|
443
|
+
tells you how each one went:
|
|
405
444
|
|
|
406
445
|
```python
|
|
407
446
|
m = boxd.machines.get("mike-dev")
|
|
@@ -410,18 +449,44 @@ for p in m.packages:
|
|
|
410
449
|
print(p.state, p.exit_code, p.command)
|
|
411
450
|
```
|
|
412
451
|
|
|
413
|
-
`state` is `"pending"`, `"running"`, `"done"` or `"
|
|
414
|
-
after a failure stay `"pending"`.
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
452
|
+
`state` is `"pending"`, `"running"`, `"done"`, `"failed"` or `"skipped"`; the
|
|
453
|
+
commands after a failure stay `"pending"`. A pending or failed command that an
|
|
454
|
+
`update_spec` removes from the spec becomes `"skipped"`: it never runs and no
|
|
455
|
+
longer stops the ones after it, which run right away (or when the machine next
|
|
456
|
+
runs). A skipped one that failed keeps its `exit_code` and `output_tail`, and
|
|
457
|
+
`reason` says when it was skipped. Listing it in the spec again puts it back to
|
|
458
|
+
`"pending"`, in its place. So replacing a broken command is one patch: drop it
|
|
459
|
+
and add the corrected one, and the corrected one runs, with everything that
|
|
460
|
+
was waiting. A running command can't be removed (the patch is refused until it
|
|
461
|
+
ends), and removing a done one undoes nothing. `output_tail` is the last 4 KiB of the
|
|
462
|
+
command's output, with the value of every secret the machine gets shown as
|
|
463
|
+
`[secret NAME]` (only the value as it is: printed encoded or in pieces, it isn't
|
|
464
|
+
recognised). The log file isn't masked: it is root's, and root on the machine
|
|
465
|
+
can read its env anyway. `output_tail` is empty on the machines `list()` returns,
|
|
466
|
+
which leave it out to keep the list small; `get()` has it. `reason` says why a failed one has no exit code (it
|
|
467
|
+
timed out, or was interrupted too often), or why a pending one is waiting. A
|
|
468
|
+
command the platform interrupts (the machine stops, reboots, hibernates or
|
|
469
|
+
moves, or boxd restarts on the machine's host) goes back to `"pending"` and runs
|
|
470
|
+
again from the start once the machine is running, and the rest follow; after 3
|
|
471
|
+
interruptions it fails. What finished never runs again: not on a reboot, a
|
|
472
|
+
resume, a wake or a move. Restoring a snapshot marks done the commands that had
|
|
473
|
+
run on the machine the snapshot was taken from and runs the snapshot spec's
|
|
474
|
+
others (one that was running when the snapshot was taken runs again from the
|
|
475
|
+
start), then the ones the restore's spec adds; a fork keeps its source's states
|
|
476
|
+
and runs nothing. The machine doesn't auto-suspend or
|
|
420
477
|
hibernate while a command runs. At most 50 commands (a restore counts the
|
|
421
478
|
snapshot's too), each at most 4 KiB; the server names the one it refuses
|
|
422
479
|
(`spec.packages[2]: ...`). Commands that download something need the hosts in
|
|
423
480
|
the spec's `egress`, if it has one.
|
|
424
481
|
|
|
482
|
+
A spec has size limits, checked before anything else and named with what was
|
|
483
|
+
given (`spec.env: at most 128 variables (got 129)`): at most 256 KiB as JSON
|
|
484
|
+
in all; `env` at most 128 variables, each value at most 32 KiB and all values
|
|
485
|
+
together at most 64 KiB; `secrets` at most 64 names; `networks` at most 16, each
|
|
486
|
+
name at most 63 bytes; `egress` at most 256 entries, none empty (leave `egress`
|
|
487
|
+
out for no allowlist); `connections` at most 64; `packages` at most 200 listed
|
|
488
|
+
and 50 once duplicates are dropped. An `update_spec` patch is at most 256 KiB.
|
|
489
|
+
|
|
425
490
|
#### Updating a machine's spec
|
|
426
491
|
|
|
427
492
|
`update_spec` changes the spec of a machine created from one and applies what
|
|
@@ -451,23 +516,33 @@ Each change in `changes` says how it applies (`apply`):
|
|
|
451
516
|
|
|
452
517
|
| Field | `apply` |
|
|
453
518
|
|---|---|
|
|
454
|
-
| `labels`, `networks`, `egress` | `"live"` |
|
|
519
|
+
| `labels`, `networks`, `egress` | `"live"` (new egress rules are in force within about a second of the call returning) |
|
|
455
520
|
| `connections`, `agents` | `"live"`: what the machine needs (`requirements`) changes |
|
|
456
|
-
| `packages` | `"live"`: added commands run now, after the ones before them. Removing one undoes nothing (a warning says so); a command that already ran isn't run again; commands after a failed one don't run (a warning says so) |
|
|
521
|
+
| `packages` | `"live"`: added commands run now, after the ones before them. Removing one undoes nothing (a warning says so); a pending or failed one the patch removes is skipped (listed in `skipped`), so the ones after it run, and a skipped one listed again goes back to pending in its place (`unskipped`); a command that already ran isn't run again; commands after a failed one don't run (a warning says so) unless the update also retries (`retry_packages=True`: the failed command and the rest run again first, listed in `changed`, skipped ones excepted; a failed command the patch removes is skipped, not retried). Removing a running command is `"refused"` until it ends |
|
|
457
522
|
| `env`, `secrets` | `"reboot_recommended"`: new shells and exec sessions get them now; processes already running keep the old values until they restart (`reboot_recommended`) |
|
|
458
523
|
| `size` | `"reboot"` on a running machine, which reboots to apply it (as `resize`), so it needs `reboot=True`; `"live"` on a stopped one |
|
|
459
524
|
| `base`, `isolated`, `assignTo` | `"refused"`: fixed when the machine is created |
|
|
460
525
|
|
|
461
|
-
A refused change (also: new packages on a fork, a size change in standby or
|
|
526
|
+
A refused change (also: new packages or a retry on a fork, a size change in standby or
|
|
462
527
|
hibernated) refuses the whole patch, and so does a reboot without
|
|
463
|
-
`reboot=True`; nothing is applied.
|
|
528
|
+
`reboot=True`; nothing is applied. So is an update during which the spec
|
|
529
|
+
changed (another update, a label edit): it raises `ConflictError`, and
|
|
530
|
+
reading the machine again and retrying is safe. The builder form does that
|
|
531
|
+
itself: it sends the spec it read, so a change made in between can't be
|
|
532
|
+
undone by its lists, and on a conflict reads the spec again and reapplies the
|
|
533
|
+
function, up to 3 times. A patch you built from a spec you read can say so
|
|
534
|
+
too, `expected_spec=machine.spec`: if the spec is no longer that one, nothing
|
|
535
|
+
is applied and it raises `ConflictError`. Otherwise the spec and the labels,
|
|
464
536
|
networks, egress and secrets it drives change together, then the size, then
|
|
465
|
-
the new packages run.
|
|
537
|
+
the new packages run. If the size then fails (the org's quota changed, the
|
|
538
|
+
machine went to standby), the rest stays applied and the call still returns:
|
|
539
|
+
`resize_error` says why, and `spec` has the size the machine has. The patched spec is checked like a create's (an
|
|
466
540
|
unknown toolkit, a secret you may not list, more than 50 commands in all are
|
|
467
541
|
refused, naming the field). `machine.created_spec` never changes, so the
|
|
468
542
|
console shows the spec badge as "updated". Allowed to whoever may edit the
|
|
469
543
|
machine's labels: its owner, or an org owner or admin on a shared machine.
|
|
470
|
-
The builder form only adds: a `size` that differs from the spec's raises
|
|
544
|
+
The builder form only adds: a `size` that differs from the spec's raises
|
|
545
|
+
(`"2X8"` is the same as `"2x8"`), so
|
|
471
546
|
use a patch to replace or remove. `spec_patch(from_spec, to_spec)` computes the
|
|
472
547
|
patch the builder form sends. `AsyncBoxd` has the same `await
|
|
473
548
|
boxd.machines.update_spec(...)`.
|
|
@@ -479,15 +554,20 @@ A machine can be limited to what it may reach on the internet:
|
|
|
479
554
|
```python
|
|
480
555
|
boxd.machines.set_egress_allow("alpha", ["api.stripe.com", "*.github.com", "203.0.113.0/24"])
|
|
481
556
|
boxd.machines.get("alpha").egress_allow
|
|
557
|
+
boxd.machines.set_egress_allow("alpha", ["none"]) # nothing is reachable
|
|
482
558
|
boxd.machines.set_egress_allow("alpha", []) # clear: unrestricted again
|
|
483
559
|
```
|
|
484
560
|
|
|
485
|
-
Entries are hostnames or wildcards, or public IPv4 addresses and CIDRs
|
|
561
|
+
Entries are hostnames or wildcards, or public IPv4 addresses and CIDRs, at
|
|
562
|
+
most 256; an internationalized name is kept in its ASCII form (`bücher.de`
|
|
563
|
+
becomes `xn--bcher-kva.de`), which is what the machine looks up. Web
|
|
486
564
|
requests are admitted by hostname, everything else by address, with the
|
|
487
565
|
addresses of allowlisted names learned as the machine resolves them. Nothing
|
|
488
566
|
that is not named is reachable. The hosts of any bound secret the machine
|
|
489
|
-
holds are always admitted.
|
|
490
|
-
|
|
567
|
+
holds are always admitted. `"none"` on its own leaves the allowlist on with
|
|
568
|
+
nothing in it, so the machine reaches only those; next to other entries it is
|
|
569
|
+
dropped. Takes effect within a second. From inside the
|
|
570
|
+
machine it can be read (`boxd machine egress`) but not changed. A fork inherits its source's allowlist; a
|
|
491
571
|
machine restored from a snapshot starts unrestricted, unless its machine spec
|
|
492
572
|
lists `egress`.
|
|
493
573
|
|
|
@@ -554,6 +634,7 @@ machine.shared # shared with that org, or private to you
|
|
|
554
634
|
machine.access.ssh_port # int | None — None until allocated
|
|
555
635
|
machine.access.domain
|
|
556
636
|
machine.access.url # https://<name>.<domain>
|
|
637
|
+
machine.ssh # bool — False: SSH is off, nothing answers on ssh_port
|
|
557
638
|
|
|
558
639
|
machine.idle.suspend_after # seconds; 0 = that timer is disabled
|
|
559
640
|
machine.idle.hibernate_after
|
|
@@ -576,6 +657,7 @@ machine.requirements # list[MachineRequirement] — the spec's connec
|
|
|
576
657
|
# kind, name, scope ("personal"/"shared"), tag, met, can_fix, reason
|
|
577
658
|
machine.packages # list[MachinePackage] — the spec's packages and how each went:
|
|
578
659
|
# command, state, exit_code, output_tail, reason, started_at, finished_at
|
|
660
|
+
# (output_tail is empty from list(): get() has it)
|
|
579
661
|
```
|
|
580
662
|
|
|
581
663
|
`None` always means "not set": a port that was never allocated, a boot that
|
|
@@ -703,6 +785,47 @@ relative to the VM user's home directory (`/home/boxd`) — a bare filename
|
|
|
703
785
|
like `"notes.txt"` resolves to `/home/boxd/notes.txt`, same as `"./notes.txt"`
|
|
704
786
|
or `"sub/notes.txt"` would.
|
|
705
787
|
|
|
788
|
+
### Filesystem
|
|
789
|
+
|
|
790
|
+
`machines.fs` works on a machine's files the way code works on local ones: as
|
|
791
|
+
the machine's user, seeing the same files its commands do, with failures
|
|
792
|
+
raised as the builtin `OSError`s.
|
|
793
|
+
|
|
794
|
+
```python
|
|
795
|
+
fs = boxd.machines.fs
|
|
796
|
+
|
|
797
|
+
fs.write(id, "/app/config.json", '{"debug": true}') # creates missing parents
|
|
798
|
+
fs.write(id, "big.bin", chunks) # or stream an iterable of bytes
|
|
799
|
+
data = fs.read(id, "/app/config.json") # bytes
|
|
800
|
+
for chunk in fs.read_stream(id, "big.bin"): ... # without holding it all
|
|
801
|
+
|
|
802
|
+
entry = fs.stat(id, "/app") # FsEntry: name, path, type, size, mode, mtime, ...
|
|
803
|
+
entry.is_dir, entry.is_symlink
|
|
804
|
+
fs.list(id, "/app") # one level, every entry
|
|
805
|
+
fs.exists(id, "/app/missing") # False
|
|
806
|
+
fs.mkdir(id, "/app/a/b") # mkdir -p
|
|
807
|
+
fs.rename(id, "/app/a", "/app/c")
|
|
808
|
+
fs.remove(id, "/app/c", recursive=True)
|
|
809
|
+
fs.realpath(id, "/app/link")
|
|
810
|
+
|
|
811
|
+
try:
|
|
812
|
+
fs.read(id, "/app/nope")
|
|
813
|
+
except FileNotFoundError:
|
|
814
|
+
...
|
|
815
|
+
```
|
|
816
|
+
|
|
817
|
+
A write replaces the file all at once, so readers see the old contents or the
|
|
818
|
+
new, never a mix; only a file that can't be swapped by a rename (several hard
|
|
819
|
+
links, another user's file, a mount point) is overwritten in place. It writes
|
|
820
|
+
through a symlink to its target, and an existing file keeps its mode and owner
|
|
821
|
+
(or takes `mode=`). Only regular files can be read. Paths are `str`, `bytes` or
|
|
822
|
+
`os.PathLike`; a relative path starts in the user's home directory, and names
|
|
823
|
+
that aren't valid UTF-8 round-trip. Errors are
|
|
824
|
+
`MachineFilesystemError` subclasses that are also `FileNotFoundError`,
|
|
825
|
+
`PermissionError`, `FileExistsError`, `IsADirectoryError` or
|
|
826
|
+
`NotADirectoryError`, with `errno` and `filename` set. A machine started before
|
|
827
|
+
`machines.fs` existed raises `MachineAgentOutdatedError` until it is rebooted.
|
|
828
|
+
|
|
706
829
|
### Ports and proxies
|
|
707
830
|
|
|
708
831
|
```python
|
|
@@ -794,18 +917,28 @@ boxd.secrets.delete("API_TOKEN", scope="shared")
|
|
|
794
917
|
```
|
|
795
918
|
|
|
796
919
|
A secret can be bound to the hosts it may be sent to. The machine then only
|
|
797
|
-
ever holds a placeholder, and the real value is substituted into
|
|
798
|
-
those hosts on the way out:
|
|
920
|
+
ever holds a placeholder, and the real value is substituted into HTTPS
|
|
921
|
+
requests to those hosts on the way out:
|
|
799
922
|
|
|
800
923
|
```python
|
|
801
924
|
boxd.secrets.set("STRIPE_KEY", "sk_live_...", scope="all", domains=["api.stripe.com"])
|
|
802
925
|
```
|
|
803
926
|
|
|
804
|
-
Inside the machine, `STRIPE_KEY` is an opaque `bxds_…` string.
|
|
805
|
-
`api.stripe.com` carrying it
|
|
806
|
-
arrives at Stripe with the real key
|
|
807
|
-
|
|
808
|
-
|
|
927
|
+
Inside the machine, `STRIPE_KEY` is an opaque `bxds_…` string. An HTTPS
|
|
928
|
+
request to `api.stripe.com` carrying it anywhere (the URL, a header, Basic auth
|
|
929
|
+
included, or the body) arrives at Stripe with the real key. It is replaced only
|
|
930
|
+
over HTTPS, and only when both the server name the connection was opened for
|
|
931
|
+
and the request's own `Host` are hosts the secret is bound to: over plain HTTP
|
|
932
|
+
and in a request to any other host it stays the useless placeholder. A body
|
|
933
|
+
over 16 MiB with a `Content-Length` is sent as written, placeholder included.
|
|
934
|
+
Responses on those connections carry the placeholder instead of the value:
|
|
935
|
+
wherever the real value (8 characters or more) appears in a response's headers
|
|
936
|
+
or body exactly as it was sent, it is replaced, so a server that echoes the
|
|
937
|
+
request doesn't hand the value to the machine. A value the server sends back
|
|
938
|
+
re-encoded (base64, URL- or JSON-escaped) isn't caught, and neither is one in
|
|
939
|
+
a body compressed with anything but gzip or deflate (the proxy asks for an
|
|
940
|
+
uncompressed response). Nothing to configure in the machine, and the
|
|
941
|
+
placeholder does not change when the value is rotated. `domains` is the full desired set:
|
|
809
942
|
setting the secret again without it makes it a plain secret. Wildcards such as
|
|
810
943
|
`*.stripe.com` are accepted; provider-wide ones such as `*.amazonaws.com` are
|
|
811
944
|
refused.
|
|
@@ -826,7 +959,7 @@ Scope decides which machines a name applies to:
|
|
|
826
959
|
| `private` | only your machines in that organization |
|
|
827
960
|
| `shared` | the organization's shared machines |
|
|
828
961
|
| `all` | every machine in the organization |
|
|
829
|
-
| `unattached` | secrets only: just the machines whose spec lists it ([Secrets](#secrets)); the organization's with `org
|
|
962
|
+
| `unattached` | secrets only: just the machines whose spec lists it ([Secrets](#secrets)); the organization's with `org` (set, moved and deleted by its owners and admins only), else your own |
|
|
830
963
|
| `private-unattached` | secrets only: your own unattached secret in the organization named by `org` |
|
|
831
964
|
|
|
832
965
|
`list` reports the organization's unattached secrets as `unattached` and your
|
|
@@ -1098,7 +1231,7 @@ from boxd import (
|
|
|
1098
1231
|
AuthenticationError, # no usable credential, or it was rejected
|
|
1099
1232
|
PermissionDeniedError, # authenticated, but not allowed
|
|
1100
1233
|
NotFoundError,
|
|
1101
|
-
ConflictError, # already exists,
|
|
1234
|
+
ConflictError, # already exists, fights the current state, or it changed meanwhile
|
|
1102
1235
|
RateLimitError, # rate limit or quota
|
|
1103
1236
|
APIStatusError, # any other error from the server
|
|
1104
1237
|
APIConnectionError, # could not reach the server
|
|
@@ -1112,9 +1245,14 @@ except NotFoundError:
|
|
|
1112
1245
|
|
|
1113
1246
|
Every error carries `.message`, `.code` (the canonical status name, e.g.
|
|
1114
1247
|
`"not_found"`) and `.grpc_code`, the numeric
|
|
1115
|
-
[status code](https://grpc.github.io/grpc/core/md_doc_statuscodes.html).
|
|
1116
|
-
|
|
1117
|
-
|
|
1248
|
+
[status code](https://grpc.github.io/grpc/core/md_doc_statuscodes.html). Some
|
|
1249
|
+
also carry `.reason`, a name for the exact failure (`"invalid_api_key"`,
|
|
1250
|
+
`"is_a_directory"`, ...), and errors from the key exchange carry
|
|
1251
|
+
`.http_status`. `machines.fs` errors are also `OSError`s (see
|
|
1252
|
+
[Filesystem](#filesystem)).
|
|
1253
|
+
|
|
1254
|
+
Connection failures are retried with exponential backoff, `max_retries` times,
|
|
1255
|
+
including failing to reach the key exchange or a machine's automatic credential.
|
|
1118
1256
|
Timeouts are never retried — the server may already have applied the request —
|
|
1119
1257
|
and neither is `AuthenticationError`.
|
|
1120
1258
|
|