安装
除标准库外没有任何依赖;需要 Python 3.9 或更高版本。调用是阻塞式的,任意多个线程都可以同时发起调用。PyPI 上的包名是 glac:
pip install glac客户端
from glac import Glac, Sandbox
glac = Glac() # GLAC_API_KEY and GLAC_API_URL; or Glac(api_key=..., base_url=...)
app = glac.cube.deploy("ghcr.io/acme/api:1.4", name="api", port=8080) # Cube: waits until it serves
print(app.url)
sbx = glac.sandboxes.create(image="python:3.12") # Sandbox
print(sbx.run("python3 -c 'print(6*7)'").stdout)
drive = glac.drives.create(name="data", size_bytes=10 << 30) # Drive: disks that outlive sandboxes
print(Sandbox.run("echo hello").stdout) # the shorthand: a sandbox for one command, on the default clientglac.cube.deploy 就是 glac deploy:为名为 api 的应用部署一个新版本(没有这个应用时先创建它),并等到它承接应用的流量。最后一行是唯一的简写,即沙箱 SDK 都有的、无需客户端的一行写法。with glac.sandboxes.create() as sbx: 会在代码块结束时删除沙箱;除此之外,除了一次性的沙箱和你自己调用的 delete(),什么都不会被删除。
Glac() 从 GLAC_API_KEY 读取密钥,从 GLAC_API_URL 读取地址(默认 https://api.glac.ai)。
| 调用 | 作用 |
|---|---|
Glac(api_key=, base_url=, timeout_seconds=, connect_timeout_seconds=, retries=) |
地址、密钥、超时和重试。任意多个线程都可以共享一个客户端。 |
glac.cube |
方块:应用(见下文)。glac.workers 是它的另一个名字,使用时会警告它已弃用。 |
glac.sandboxes, .snapshots, .ssh_keys |
沙箱、沙箱的快照,以及能打开沙箱的 SSH 密钥(见下文)。 |
glac.drives, glac.volumes, glac.backups |
云盘(属于控制平面)、云盘底层的存储卷,以及存储卷的备份(属于控制平面;见下文)。 |
glac.databases |
数据库:托管的 Postgres,以及可以直接交给 psycopg 的连接字符串(见下文)。 |
glac.domains |
项目的自定义域名,即应用或沙箱某个端口的名称(见下文)。 |
glac.webhooks, glac.keys |
项目的事件发往哪里,以及项目的 API 密钥(见下文)。 |
glac.destinations |
项目中应用的日志、指标和追踪导出到哪里(见下文)。 |
glac.secrets, glac.variables |
项目的机密和共享变量(见下文)。 |
glac.deploys |
项目的部署(/v1/deploys,属于控制平面):一起部署的多个服务(见下文)。 |
glac.account(), update_account, usage, billing, update_billing, balance, update_balance, top_up(usd), charges, ledger, project, events |
平台本身的、不属于任何产品的调用(见下文)。 |
glac.request("GET", "/v1/webhooks"), glac.pages(path) |
SDK 没有封装的 API:返回它的响应 JSON;列表则逐页返回其中的每一项。 |
各产品是命名空间,每个都持有访问它所用的客户端。对象沿用 API 的名称:方块是 App(API 称之为 app),还有 Sandbox、Snapshot、Drive、DriveSnapshot(云盘的快照)、Volume、Backup(存储卷的备份)、Domain、SshKey、Secret、Variable、Version、Instance、Deploy。对某个对象的操作就是它自己的方法:sbx.run、app.deploy、drive.delete。
简写。Sandbox.run、Sandbox.create、Sandbox.get、Sandbox.get_or_create 和 Sandbox.list 就是 glac.sandboxes 的调用,使用的客户端在第一次需要时根据环境变量创建,之后共用;client= 可以指定另一个客户端。它们是同一套代码。其他东西都需要客户端。
有些选项中的 None 有特定含义(sleep_after_seconds=None 表示永不休眠,env={"KEY": None} 会删除这个键),所以没有给出的选项为 UNSET (from glac import UNSET),保持原样。
方块
方块(API 称之为 app)是作为服务运行在自己地址上的镜像,实例数量随请求的需要而定,没有请求时一个都没有。每次部署都是一个版本,版本就绪后承接流量。应用属于控制平面:对着一个独立运行的节点,每个调用都会得到该节点的 404 no_such_route。
| 调用 | 作用 |
|---|---|
glac.cube.deploy(image, name=, ...) |
把镜像作为方块部署。部署到 name 指定的应用时:先修改给出的应用自身设置(regions、scale、metadata、keys、egress、logs、traces、public、depends_on、network),然后用镜像和其余设置创建一个版本。没有这个应用,或不给名称时:用全部设置创建应用及其第一个版本。在此期间被别人占用的名称 (409 name_taken) 视为部署到他们的应用。版本承接流量后返回应用。 |
glac.cube.create(name=, image=, port=, env=, secrets=, volumes=, drives=, key_drives=, ..., wait=, timeout_seconds=), get(id_or_name), get_or_create(name, ...), list() |
创建一个应用(给出 image 时,同时创建它的第一个版本,并等待它);查找一个;查找或创建一个,不部署任何东西;列出。drives 是应用唯一的实例以读写方式挂载的云盘(ServiceDrive;有多个实例或使用键的应用会被拒绝,409 drive_single_writer);key_drives 给键控应用的每个键的实例各自的云盘 (KeyDrive),随该键的第一个实例创建,随键删除。 |
app.deploy(image=, env=, ...), app.version(n), app.versions(), app.rollback(version) |
一个新版本,等到它承接流量,作为 Version 返回;查看一个版本;所有版本;把流量切到另一个版本。 |
app.update(...), app.refresh(), app.delete() |
修改(修改构成版本的内容会产生新版本);重新读取;删除。 |
app.managed_by |
唯一能修改这个应用的东西(一个 ManagedBy)。它所属的数据库,{"object": "database", "id": "db-…"}:在这里对它更新、部署、回滚或删除都会被拒绝,返回 409 managed_by_database(它的日志和其他应用一样可以读取和删除)。或者是沙箱产品,{"object": "sandboxes", "id": None}:项目默认的沙箱应用 sbx-sandbox,或某个沙箱所用镜像对应的应用,它的实例就是项目的沙箱;除了指定默认应用的镜像 (app.update(image=)),其他修改都会被拒绝,返回 409 managed_by_sandboxes,但可以删除它。不属于任何东西的应用为 None。 |
glac.cube.create、deploy、get_or_create、app.deploy 和 app.update 的 runtime= |
版本的运行时设置,一个 ServiceRuntime:ulimits ({"nofile": 65535})、sysctls、tmpfs、dns、shm_size_bytes;整体替换。 |
app.logs(follow=, since=, kinds=, status=, contains=, cursor=, ...), app.log_usage(), app.delete_logs(before=), app.logs_state |
应用的日志:OutputLine、RequestLine 和 PlatformLine,来自对象存储和各台机器,连接丢失后会接着读取;日志的统计;删除日志行;保存其日志的每台机器,现在是否长期保留那里打印的内容(日志)。 |
app.instance(key), app.get_instance(key), app.instances(), app.delete_instance(key) |
键控实例,每个键一个,每个都是一个 Instance,也就是一个沙箱。 |
instance.upgrade(version=, when=, keep_days=), app.upgrade_instance(key, version=, when=, keep_days=); upgrade.wait(timeout_seconds=); instance.upgrades(), app.instance_upgrades(key), app.instance_upgrade(key, id) |
把某个键的实例原地迁到应用的另一个版本:默认为应用的当前版本,也可以用 version 指定(id、编号或 Version;必须是就绪的版本,也可以是更旧的)。实例保留它的 id、节点、键、地址、令牌,以及它自己的云盘和其中的内容;它的磁盘、机器和进程换成新版本的,打开的连接会结束,切换期间它的请求暂存在边缘节点。返回一个 Upgrade(status 为 preparing、switching、done 或 failed;from_version、to_version、timings、error、put_back;kind(upgrade 或 revert)、when、keep_days、rollback(回退它所需的内容,保留 keep_days 天)、reverts、batch;通过 API 要求等实例休眠的升级,在那之前为 waiting,被取消时为 cancelled(API 的 DELETE,或它的批次被停止):被取消的升级就此结束,等待它的调用会像升级失败那样抛出异常,错误码为 cancelled);实例已经在那个版本上时返回 None。新版本没能起来时,wait() 抛出 UpgradeError(错误码为 upgrade_failed 或 upgrade_not_here):这时实例留在原来的版本上。instance.refresh() 重新读取实例,其中包括它的 version_id。when="asleep" 让升级等到实例休眠时再开始 (waiting),这样它的请求一个都不会被暂存;keep_days(0 到 365,默认 7)是升级完成后回退所需内容的保留天数,按快照计费。如果版本的服务挂载的云盘或存储卷与实例的不同,就不能原地升级:升级以 failed 结束,错误码为 upgrade_not_here。 |
upgrade.cancel(), app.cancel_instance_upgrade(key, upgrade); upgrade.revert(keep_days=), app.revert_instance_upgrade(key, upgrade, keep_days=) |
取消一个正在等待的升级(等实例休眠,或在批次中排队):状态变为 cancelled,实例留在原来的版本上(已经开始的升级返回 409 upgrade_started)。回退:把实例恢复到升级所保留的状态,即它的磁盘、升级之前的云盘(会先为云盘现在的内容创建快照)和原来的版本;返回一个新的 Upgrade,kind 为 revert,reverts 指明被回退的升级,它的进行和等待方式与升级相同,并且也会保留回退它自己所需的内容。要回到的内容已不再保留时,返回 409 rollback_gone。 |
app.upgrade_instances(version=, from_versions=, keys=, when=, keep_days=, concurrency=, max_failures=); app.upgrade_batches(), app.upgrade_batch(id), app.stop_upgrade_batch(id); batch.wait(timeout_seconds=), batch.refresh(), batch.stop() |
把应用的多个键批量升级到同一个版本(默认为应用的当前版本),可以限定为 from_versions 中的版本、keys 中的键:每个键在其实例休眠时升级(when="asleep",默认)或轮到时立即升级 ("now"),同时 concurrency 个(默认 4;同一节点上 2 个),失败数达到 max_failures(默认 5)时以 failed 停止。返回一个 UpgradeBatch:status(running、done、failed、stopped)、stop_reason、counts(total、waiting、preparing、switching、done、failed、cancelled)、upgrades(每个成员的键、升级、状态和错误)、skipped(自己的升级尚未结束的键)、origin(api,或在应用的 keys 设置了 upgrade_on_deploy 时为 deploy)。无论批次如何结束,wait() 都返回它,不抛出任何异常;实例从不休眠的成员会让批次一直处于 running,所以 timeout_seconds 会抛出 GlacTimeoutError。stop() 取消仍在等待的成员。 |
app.open_instance_port(key, port, from_=[...]), app.instance_ports(key), app.close_instance_port(key, port) |
某个键的实例在它自己的公网 IPv6 地址上开放的端口,无论实例在哪里:按应用和键调用它的沙箱的 open_port、ports 和 close_port(protocol="udp" 表示 UDP;键控实例)。没有自己地址的实例会被拒绝 (409 no_ipv6)。 |
glac.cube.create、deploy、get_or_create 和 app.update 的 checkpoint=;app.checkpoint;sbx.checkpoint |
应用的键控实例在运行期间的检查点(一个 AppCheckpoint,{"every_seconds": n}:1 到 600,省略或为 None 时为 10;app.update(checkpoint=None) 去掉检查点),以及实例的检查点进行得如何(一个 CheckpointStatus:every_seconds、last_at、behind_seconds、bytes_per_second、stored_bytes、error;没有检查点时为 None)。 |
app.metrics(start=, end=, step=) |
为应用提供服务的机器对它的统计,每分钟一步(step="hour":每小时一步):按状态码类别统计的请求数、耗时、实例及其用量(指标)。返回 API 的对象。 |
glac.cube.create、deploy、get_or_create 和 app.update 的 traces=;app.traces |
平台为应用的一部分请求创建 span,实例自己的 span 是它的子 span(一个 AppTraceSettings:{"sample_rate": 0.1, "trust_incoming": False};默认不创建),发往项目中接收 "traces" 的导出目标(追踪);应用当前的设置。 |
app.create_key_secret(), app.key_secrets(), app.key_secret(id), app.delete_key_secret(id), glac.sign_key(secret, app_id, key, expires_at=, path=) |
用来签名应用的键的机密(一个 KeySecret,只有刚创建的那个才有 secret),以及用它签名的键,即经由边缘节点的请求所携带的形式(签名键);path=True 把它写成路径段。 |
from glac import DeployError, Glac
glac = Glac()
app = glac.cube.deploy("public.ecr.aws/docker/library/python:3.12-alpine", name="hello",
start_command=["python3", "-m", "http.server", "8000"], port=8000) # waits for version 1
print(app.url)
app.deploy(env={"STAGE": "prod"}) # a new version; waits until it has the traffic
try:
app.deploy(start_command="exit 3")
except DeployError as e: # the traffic stays where it was
print(e.code, e.version.number)
app.rollback(1)
for line in app.logs(follow=False):
print(line)会构建版本的调用会等待版本,除非要求不等 (wait=False);版本失败时抛出 DeployError,附带版本自己的错误码和输出。timeout_seconds= 会在这么久之后抛出 GlacTimeoutError,部署仍会继续。
用 glac.deploys 一起部署项目中的多个服务(项目部署):控制平面按各服务等待的对象排序(先是有状态服务,然后是任务,然后是其余服务,并一起切换),失败的部署不会切换任何尚未切换的东西。
d = glac.deploys.create({"api": {"image": "ghcr.io/acme/api:2"}, "web": {}}) # waits until it is over
print(d.status, [(s.service, s.status, s.version) for s in d.services])create(services, wait=True, timeout_seconds=None) 以 API 的方式接受每个服务的版本字段({}:按原样),并等待部署完成 (Deploy.wait),跟随取代它的部署;失败时抛出 DeployError,附带 deploy、service 和部署的 code(job_failed、job_timed_out、job_lost 等)。get(id) 和 list()(最新的在前)读取部署;Deploy 包含 status、cause、services (DeployService)、error、replaced_by、steps 以及各个时间。
沙箱
| 调用 | 作用 |
|---|---|
glac.sandboxes.create(image=, snapshot=, name=, env=, metadata=, volumes=, drives=, public_ports=, sleep_after_seconds=, sleep_notice_ms=, sleep_hold_max_ms=, expires_at=, egress=, start=, start_command=, ready=, project_network=, hostname=, stop=, checkpoint=, image_pull_secret=) |
创建一个沙箱;每个选项都可以省略。drives=[DriveMount("/data", drive)] 挂载云盘(见下文)。启动时运行的服务:start=True(镜像自己的启动命令)或 start_command=,配合 ready={"tcp": True, "port": 5432}(或 {"http": "/health", "port": 8080}、{"command": "…"});hostname= 设置机器的名称,stop={"signal": "SIGINT", "grace_ms": 30000} 设置服务的停止方式;project_network=True 让它从一开始就加入项目的私有网络,地址为 sbx.project_address;VolumeMount("/data", vol, copy=True) 把镜像在该路径下的内容放进新的存储卷(创建沙箱)。 |
sbx.app_id, sbx.version_id, sbx.key, sbx.image_digest |
通过控制平面创建的沙箱是某个应用的实例:不带镜像创建时,是项目默认的沙箱应用 (sbx-sandbox),否则是它的镜像对应的应用(glac.apps.get(sbx.app_id).instances() 列出该应用的沙箱,每个都在它的键下);它所属的该应用的版本、创建它所用的镜像(之后被移动过的标签会成为应用的新版本,此后创建的沙箱属于新版本);它在那里的键、名称或 id;以及按摘要标识的镜像。创建的响应和之后的每次读取都会给出这些信息,并且保持不变;从模板或快照创建的沙箱,以及分叉,这些信息为 None。 |
glac.sandboxes.get(id_or_name), get_or_create(name, ...), list(status=, metadata=) |
查找一个;查找或创建一个;列出。 |
sbx.start_command |
它的启动命令如何运行,按客户机最近一次的报告(一个 StartCommandState):status(running;restarting,在 restarting_at 重新启动;exited)、restarts(键控实例和有状态服务的实例,在命令自行结束时会在机器中重新启动,每次启动都是一个 instance.restarted 事件)、last_exit(status、signal、at);节点没有报告时为 None。Instance 也有它。 |
create、get_or_create 和 sbx.update 的 suspend_after_ms=、wake_daily_at=;sbx.suspend_after_ms、sbx.wake_daily_at |
因空闲而停止后,它保持暂停多久才保存内存、真正休眠(一秒到一天;它的节点最多保持十分钟),以及休眠期间每天在 UTC 的哪个时刻 ("HH:MM") 唤醒它,以便做它自己等着唤醒才做的工作;在更新中设为 None 会把其中任意一项恢复默认。 |
create、get_or_create 和 sbx.update 的 on_expiry=、held_asleep=;sbx.on_expiry、sbx.held_asleep |
expires_at 到期时做什么:"delete" 删除沙箱(默认)、"sleep"(让它休眠并保留,请求照常会唤醒它)或 "hold"(同上,但保持休眠);以及它休眠时,发往它某个端口的请求是否会唤醒它:保持休眠时,它的预览地址返回 502 sandbox_paused,API 调用会唤醒它。在更新中设为 None 会把其中任意一项恢复默认。 |
create、get_or_create 和 run 的 init="systemd";sbx.init |
完整的系统:镜像自己的 systemd 作为沙箱的 PID 1(不带 image 时,使用平台带 systemd 和 OpenSSH 的 Ubuntu),所以软件包、用 systemctl 启动的服务、cron 和 journal 都能像在一台普通机器上一样工作,它也有自己的 machine ID,在休眠、唤醒和迁移后保持不变。不能与 start、start_command 或 ready 同时使用 (400 invalid_start),也不能与 snapshot 同时使用。这样的沙箱 sbx.init 为 "systemd",使用平台 init 的沙箱则为 None。 |
glac.sandboxes.run(command, ...) |
为一条命令创建的沙箱:创建、使用、删除。 |
sbx.run(command, cwd=, env=, user=, timeout_seconds=, stdin=, on_stdout=, on_stderr=) |
运行并等待,无论多久。返回一个 Result。 |
sbx.exec(command, stdin_open=, tty=) |
启动命令并立即返回一个 Command:wait()、output()、write()、kill()。 |
sbx.terminal(command=, cols=, rows=, join=) |
一个终端。 |
sbx.files |
read、read_text、write、list、stat、exists、mkdir、move、remove,以及 set_attributes(path, mode=, size_bytes=, modified_at=):文件的权限、长度和最后修改时间。 |
sbx.files.upload(local_path, path), download(path, local_path), read_stream(path, offset=, length=, chunk_size=), write(path, file_or_iterable) |
任意长度的文件都可以在一次调用中传输,无论这里还是 API 都不会持有整个文件:upload 边读本地文件边发送,download 随字节到来写入本地文件(所有字节都到齐后才放到位),read_stream 一段一段地给出字节,write 也接受以二进制方式打开的文件 (open(p, "rb")) 或任何字节的可迭代对象。响应在达到其长度之前就结束时,会抛出 GlacConnectionError:请重新读取。 |
sbx.url(port), sbx.rotate_urls(), sbx.token(scopes=, expires_in_seconds=) |
端口链接(在本地签名,不发请求);新的链接机密;只用于这个沙箱的令牌。 |
sbx.ipv6, sbx.open_port(port, from_=[…], protocol="tcp"), sbx.ports("tcp"), sbx.close_port(port, protocol="tcp"), sbx.update(inbound=[…]) |
它自己的公网 IPv6 地址(如果有,否则为 None);向列出的来源(IPv6 前缀或地址,"::/0" 表示所有人)开放一个端口,并给出访问地址;开放的端口;关闭一个端口(返回它之前是否开放);完整的列表,每项是一个包含 protocol、port 和 from 的 dict。入站默认关闭;它休眠时和迁移时,开放的端口都保持开放。参见网络。 |
sbx.revoke_token(token) |
从此只拒绝那一个令牌,其他令牌照常有效:一个 Token(它的 id,即 tok_…,指明是哪个)、令牌本身,或它的 id。一个沙箱最多保留 1,000 个尚未过期的这类吊销(超出时返回 409 too_many_revoked_tokens:这时请用 revoke_tokens())。返回一个 TokenRevocation,带有 token_id 和 expires_at(令牌过期的时间,吊销也随之失效)。 |
sbx.revoke_tokens() |
到目前为止为这个沙箱发出的所有令牌都会失效 (401 invalid_token),包括预览链接和 Cookie 中的令牌,以及 SSH 会话持有的令牌;之后创建的令牌有效。需要 API 密钥。返回一个 TokenRevocation:sandbox_id、generation、revoked_at。 |
sbx.update(...), sbx.refresh(), sbx.sleep(), sbx.restart(), sbx.delete() |
修改;重新读取;立即休眠;从磁盘启动;删除。 |
glac.sandboxes.create 和 sbx.update 的 checkpoint=;sbx.checkpoint |
运行期间的检查点({"every_seconds": 10};sbx.update(checkpoint=None) 去掉检查点),这样机器丢失时它能带着进程继续运行;检查点的进行情况(一个 CheckpointStatus)。没有机器支持检查点时返回 409 checkpoints_unavailable。 |
sbx.disk_writes |
它的磁盘今天(UTC 日)已写入的量,与当天全速份额的对比(一个 SandboxDiskWrites):today_bytes,其中 checkpoint_copy_bytes 是检查点副本写入宿主机磁盘的量;full_speed_until_bytes,即份额(默认 100 GiB,节点另有设置时除外;None 表示节点不限速,但照样统计);limited 和 limited_until:超出份额后,它的磁盘以每秒 2 MiB 的速度接受写入,直到当天结束,任何写入都不会失败。在不把磁盘复制到对象存储的节点上为 None,那里什么都不统计。写入它的云盘的数据不计在这里:写入云盘不收费。 |
sbx.snapshot(name=), sbx.fork(count), sbx.restore(snapshot) |
创建快照;分叉;原地回到某个快照。 |
glac.sandboxes.snapshots.get(id_or_name), .list(sandbox_id=), snap.update(name=, expires_at=) |
快照,以及每个快照是否已进入对象存储 (replicated);给快照换个名称(None:不要名称),或设置它自行删除的时间(None:永不删除)。 |
glac.sandboxes.ssh_keys.create(public_key, sandbox_id=, expires_at=), .get, .list |
通过 SSH 网关打开沙箱的公钥;私钥会在发送任何东西之前就被拒绝。 |
sbx.run_code(code), sbx.agent("claude"), Desktop(sbx) |
有状态的代码执行、编程智能体,以及桌面的屏幕,运行在从代码解释器、智能体和桌面镜像创建的沙箱中。 |
glac.sandboxes.run(以及类上的 Sandbox.run)为一条命令创建沙箱;sbx.run 在沙箱 sbx 中运行一条命令。实时输出:
res = sbx.run("make", on_stdout=lambda text: print(text, end="")) # watch and wait
cmd = sbx.exec("node server.js") # do not wait
for piece in cmd.output(): # piece.stream, .text, .data, .offset
print(piece.text, end="")输出就是命令写出的原始字节。res.stdout 是 str(UTF-8,无法解码的字节会被替换),res.stdout_raw 是 bytes。
云盘
云盘是项目的一块磁盘,不随它的沙箱消失:同一时间由一个沙箱以读写方式挂载,可以在沙箱创建时挂载,也可以在沙箱运行时挂载;沙箱没了,云盘连同其中的内容一起保留,供下一个沙箱挂载:可以在它的区域,也可以在别的区域,只要那里的机器能读取它归属区域的对象存储(见下文)。云盘在写入的同时被复制到所在区域的对象存储中,所以其他节点上的沙箱也能挂载它:会先把它搬过去。只按写入的数据计费 (used_bytes)。它的快照是它在对象存储中的版本,任意多个沙箱可以同时以只读方式挂载,可以从快照分叉出新云盘,也可以把云盘恢复到快照(见下文)。云盘属于控制平面 (/v1/drives):对着一个独立运行的节点,每个调用都会得到该节点的 404 no_such_route,节点上的磁盘是存储卷(见下文)。
| 调用 | 作用 |
|---|---|
glac.drives.create(name=, size_bytes=, region=, durability=), get(id_or_name), list() |
创建一个云盘,里面只有文件系统;每个选项都可以省略(size_bytes:4 KiB 块的整数倍,最小 16 MiB;默认 64 GiB,免费版上为 5 GiB;region:na、eu 或 ap;durability:standard 或 strict,后者要等对象存储收到 fsync 之前的每一次写入,客户机的 fsync 才返回)。查找一个;列出,最新的在前。 |
glac.drives.update(drive, name=), glac.drives.delete(drive) |
重命名(None:不要名称);删除云盘及其中的内容。 |
glac.drives.attach(sandbox, drive, path, mode=), glac.drives.detach(sandbox, drive, force=) |
立即以读写方式把它挂载到一个已有的沙箱中(休眠的沙箱会被唤醒);卸载它(有进程在使用时用 force=True)。每个调用都返回云盘。 |
drive.attach(sandbox, path), drive.detach(sandbox), drive.update(name=), drive.refresh(), drive.delete(); drive.snapshot(name=, fs_consistent=), drive.snapshots(), drive.fork(name=, from_snapshot=), drive.restore(snapshot) |
同样的调用,也可以在 Drive 上进行(fork 返回新云盘,restore 返回云盘本身):id(vol-…:云盘就是存储卷)、name、size_bytes、used_bytes、attached_to(DriveAttachment:沙箱、路径、模式;它的快照的只读挂载不在其中)、region、home、status、lease(DriveLease:唯一写入方所在的节点、纪元和到期时间)、app_id 和 key(键控实例的云盘)、parent_drive_id 和 root_drive_id(它的来源云盘和这一脉的第一个云盘,可能已被删除;不是分叉的云盘为 None)、managed_by(一个 ManagedBy,数据库的云盘才有;不属于任何东西时为 None)、created_at。 |
glac.drives.snapshot(drive, name=, fs_consistent=), snapshots(drive), all_snapshots(drive_deleted=), get_snapshot(drive, snapshot), delete_snapshot(drive, snapshot) |
创建快照(一个 DriveSnapshot);列出一个云盘的快照,最新的在前,或列出项目中所有云盘的快照(drive_deleted=True:只列出已删除云盘的快照);查找一个;删除一个。drive 可以是 Drive、id 或名称(已删除云盘的 id);snapshot 可以是 DriveSnapshot 或它的 id (dsnap-…)。 |
glac.drives.fork(drive, name=, from_snapshot=), glac.drives.restore(drive, snapshot) |
一个新云盘,它的第一个版本是快照的内容,或云盘当前的内容:返回新云盘。把云盘恢复到它的某个快照:返回云盘。 |
glac.drives.create(..., history_seconds=), update(drive, history_seconds=); glac.drives.versions(drive, at=), fork(drive, at=), restore(drive, at=); drive.versions(at=), drive.history_seconds, drive.history |
云盘的版本窗口 (slice DB8):按时间最多能往回恢复或分叉多远,3600 到 86400 秒(不设置时没有窗口;关闭后,它保留的内容会被丢弃)。在每个有写入的 30 秒里,窗口都会得到一个版本,即云盘在某一时刻的状态;这些版本单独保留的数据和快照一样计费。按时间恢复或分叉时,取该时间点或之前最新的版本,也就是以该时间调用版本列表时给出的那个版本。versions 返回 DriveVersion(at、version、consistency、exclusive_bytes),最新的在前;restore 接受快照或 at,二者只能给一个(两个都给或都不给时,这里会抛出 ValueError);update(history_seconds=None) 关闭版本窗口。 |
glac.drives.create(..., cache_bytes=), update(drive, cache_bytes=); drive.update(cache_bytes=), drive.cache_bytes, drive.cached_bytes |
云盘的缓存:节点在 NVMe 上最多保存云盘的多少数据。默认全部保存(即云盘的大小);也可以设得更小,最小到 0,超出的部分只保存在对象存储中,客户机读到时再从那里读取(最久没被读过的数据最先释放,对象存储还没有的数据从不释放)。缓存按小时、按已填充的量计费,与云盘的数据费用分开。调小缓存后,超出的部分一旦已在对象存储中就会被释放(如果运行中的沙箱挂载云盘时是全部缓存,则从那个沙箱下一次休眠或停止时开始);调大缓存后,从那时起客户机读到的数据都会留在缓存中。API 把云盘叫作存储卷 (/v1/volumes);各客户端通过 /v1/drives 访问它们,这个接口的响应和以前一样。更新时传 cache_bytes=None 恢复为全部缓存。控制平面没有给出缓存信息时,drive.cache_bytes 就是云盘的大小;drive.cached_bytes 是当前在节点 NVMe 上的量(没有节点持有云盘时为 None)。 |
glac.drives.attach_snapshot(sandbox, snapshot, path, drive=), glac.drives.detach_snapshot(sandbox, snapshot, force=) |
立即以只读方式把快照挂载到一个已有的沙箱中;卸载这个挂载。每个调用都返回快照。用 id 给出快照时需要 drive=(DriveSnapshot 自带它的云盘);detach_snapshot 会在发送之前就拒绝任何不是快照 id 的东西 (ValueError),因为同一个路径会卸载云盘的读写挂载。 |
snap.refresh(), snap.fork(name=), snap.restore(), snap.attach(sandbox, path), snap.detach(sandbox, force=), snap.delete() |
同样的调用,也可以在 DriveSnapshot 上进行:id、drive_id(它的云盘,可能已被删除)、name、consistency(frozen、paused、at_rest)、used_bytes(创建时云盘中的数据量)、exclusive_bytes(它单独保留的数据量,也是计费依据)、exclusive_at、version(DriveSnapshotVersion:number、epoch)、created_at。 |
DriveMount(path, drive, mode=None, snapshot=None) |
用于 glac.sandboxes.create(drives=[...]) 和 get_or_create 的云盘:与存储卷合计最多 8 个。带 snapshot(DriveSnapshot 或它的 id)时,以只读方式挂载那个快照:它会发送 mode: "snapshot" 和快照的 id。 |
from glac import DriveMount
drive = glac.drives.create(name="data", size_bytes=10 << 30)
sbx = glac.sandboxes.create(drives=[DriveMount("/data", drive)])
sbx.run("echo kept > /data/note")
glac.drives.detach(sbx, drive)
other = glac.sandboxes.create()
glac.drives.attach(other, drive, "/data")
print(other.run("cat /data/note").stdout) # kept- 一个云盘同一时间只属于一个沙箱:另一个沙箱持有的云盘会被拒绝 (409
drive_attached),删除已挂载的云盘也一样。键控实例的云盘随它的键一起删除 (409key_drive)。 - 数据库的云盘(
drive.managed_by,{"object": "database", "id": "db-…"})只能由数据库的实例以读写方式挂载,重命名、恢复和删除都要通过数据库进行:在这里调用这些操作,或以读写方式挂载它,都会被拒绝 (409managed_by_database)。项目可以为它创建快照和分叉,任何人都可以以只读方式挂载它的快照。 - 另一个节点持有的云盘,会先被搬到沙箱所在的节点;对象存储中还没有它的最新状态时 (503
copy_behind),或无法把它搬过去时 (503drive_not_brought),调用为retryable:客户端会按retries的设置重试,之后抛出异常,你稍后再请求一次即可。 - 跨区域 (slice D9)。云盘的归属区域 (
home) 是保存其副本的对象存储所在的区域:也就是创建它的区域。另一个区域的沙箱也能挂载它,前提是那个区域的机器能读取其归属区域的存储(不能时返回 409not_on_one_machine):云盘被释放为一个版本,在那里被接管,客户机读取时从归属区域的存储读出,客户机写入的数据则进入它现在所在区域的存储 (region)。云盘在那里待满一小时后,归属区域会跟过去,用glac.drives.move_home(drive)(drive.move_home()) 则立即迁移:它的机器把归属区域存储中剩下的内容复制到自己区域的存储中,那里什么都不剩时,home就变成新区域;drive.home_move说明迁移进行到哪一步(asked_at、asked_by、copied_objects、copied_bytes、objects_left、error)。保留的版本都不再需要旧归属区域的副本后(一天后),这份副本会被删除。云盘的归属区域最多每六小时迁移一次 (409home_moved_recently);归属区域就是当前所在区域的云盘返回 409home_here。 - 套餐的限制:免费版最多 3 个云盘,单个最大 10 GiB,合计 15 GiB;付费版最多 500 个,单个最大 1 TiB (403
drive_limit_reached)。 glac.usage(drive=...)、kind="drive"和group_by="drive"报告云盘保存了多少、写入了多少、冷读了多少。drive.created、drive.deleted、drive.attached和drive.detached事件携带EventDrive或EventDriveAttached,drive.snapshot.created和drive.snapshot.deleted携带EventDriveSnapshot。glac.drives.attach的mode为read_write:快照的只读挂载用attach_snapshot,它返回快照(不带快照的mode="snapshot"返回 400missing_field)。
云盘的快照。快照是云盘在对象存储中的一个版本:创建快照不复制任何数据,分叉、恢复或以只读方式挂载它也不复制。
snap = drive.snapshot(name="before-upgrade") # a running sandbox has the Drive: its writes held a moment, "paused"
reader = glac.sandboxes.create(drives=[DriveMount("/before", drive, snapshot=snap)]) # read-only, any number of sandboxes
copy = glac.drives.fork(drive, name="data-copy", from_snapshot=snap)
glac.drives.detach(sbx, drive)
glac.drives.restore(drive, snap) # once no sandbox has it read-write- 运行中的沙箱持有的云盘,会在写入被暂停片刻时创建快照,最多十秒(之后返回 503
unavailable,可重试):fs_consistent=True会先冻结它的文件系统(consistency为frozen;沙箱的代理程序做不到时返回 409not_frozen),否则暂停它的机器,相当于断电 (paused)。没有运行中的沙箱持有的云盘按原样创建快照 (at_rest)。 - 删除云盘时不会删除快照:云盘删除后,仍可以按云盘的 id 列出、读取、分叉和挂载它,直到它自己被删除 (
snap.delete())。快照按它单独保留的数据 (exclusive_bytes) 计费,按小时计,价格与云盘的数据相同(每 GB·月 $0.02);glac.usage(snapshot=snap.id)会报告它。 - 分叉是一个新云盘,可以立即挂载,大小与来源云盘相同,并说明它从何而来(
parent_drive_id、root_drive_id);它计入套餐的云盘数量 (403drive_limit_reached),项目中已有的名称返回 409name_taken。它和任何云盘一样,按它的used_bytes,即完整的内容计费:从 10 GB 快照分叉出的云盘按 10 GB 的云盘计费。 - 恢复会在云盘最新的版本之后写入一个新版本,内容与快照相同;中间的版本只要有快照或分叉保留着就仍然存在,所以先恢复到较旧的快照、再恢复到较新的快照也可以。沙箱以读写方式持有云盘时,恢复会被拒绝(409
drive_attached:先卸载它)。 - 只读挂载:云盘所在区域任意节点上的任意多个沙箱都可以同时挂载,无论云盘被谁持有,云盘删除后也可以;客户机不能写入它。数据块在客户机读取时从对象存储取来:这些冷读不收费,并在用量中计在那个沙箱名下(
glac.usage(sandbox=...),drive_cold_read_bytes);那个沙箱的机器上已有的数据,读取也不收费。有只读挂载的沙箱不会迁到别的机器上。 - 云盘所在节点不向对象存储复制任何数据时,没有快照、分叉和恢复 (409
not_copied)。应用的版本只能以读写方式挂载云盘(在那里使用mode为snapshot会返回 400invalid_field):快照的只读挂载只供沙箱使用。
glac.volumes 对应 API 的 /v1/volumes,即云盘底层的磁盘,为过渡期而保留:只有它才有的,是独立运行的节点(节点上的磁盘就是存储卷),以及共享存储卷,即可由多个沙箱同时挂载的文件系统。控制平面不会为项目的密钥创建普通存储卷。
from glac import VolumeMount
vol = glac.volumes.create(name="team", shared=True) # no size: a large limit, and the space of what is written
sbx = glac.sandboxes.create(volumes=[VolumeMount("/team", vol)])
print([v.name for v in glac.volumes.list()], glac.volumes.get("team").id)已挂载的存储卷不能删除 (409 conflict):用 sbx.update(volumes=[]) 卸载它。
存储卷的备份。备份是存储卷(云盘也是存储卷)在某一时刻的副本,保存在对象存储中,直到过期:可以由调用创建,可以按存储卷的规则每天创建,也会在服务的新版本替换其实例之前创建。与云盘的快照不同,它是一份独立的副本,仅凭它就能创建存储卷,而且不随存储卷消失;存储卷的备份只保存自上一个备份以来变化的内容,多个备份都需要的数据块只计费一次,计在最早的备份上。在存储收到全部数据之前为 making,之后为 ready,从那时起按存储中保存的数据量 (stored_bytes)、以休眠沙箱所保存内容的价格计费(备份)。备份属于控制平面。
import time
b = glac.volumes.backup("data", expires_in_days=7) # "making", then "ready"
while b.refresh().status == "making":
time.sleep(1)
copy = glac.backups.restore(b, name="data-back") # a new volume: "restoring", then "ready"| 调用 | 作用 |
|---|---|
glac.volumes.backup(volume, expires_in_days=), vol.backup(expires_in_days=), glac.volumes.backups(volume) |
立即创建一个备份,即一个 Backup,保留 expires_in_days 天(1 到 365),或按存储卷的规则(未设置时为 30 天);列出一个存储卷的备份,最新的在前(已删除的存储卷按 id 指定)。不向对象存储复制任何数据的节点没有备份 (409 not_copied)。 |
glac.backups.list(volume=), get(backup), delete(backup), restore(backup, name=) |
项目的备份,最新的在前,包括已删除存储卷的备份,或一个存储卷的备份;查看一个;删除一个(之后不再为它计费;从它创建的存储卷仍然保留);从一个就绪的备份创建新存储卷,等同于 glac.volumes.create(from_backup=, name=)(创建期间返回 409 backup_not_ready,失败后返回 409 backup_unusable)。 |
b.refresh(), b.restore(name=), b.delete() |
同样的调用,也可以在 Backup 上进行:id (bak-…)、volume_id、volume_name(当时存储卷的名称)、status(making、ready、failed)、reason(manual、daily、before_version)、consistency(frozen、crash)、frozen_ms(存储卷的写入方为复制等待了多久;没有冻结时为 None)、size_bytes(存储卷当时的数据量)、volume_size_bytes(它最多能容纳的量,也就是从备份创建的存储卷的容量)、stored_bytes(计费依据)、own_bytes(删除它能省下的量)、shared_bytes、shared、region、failure、created_at、ready_at(从此开始计费)、expires_at。 |
- 从备份创建的存储卷,大小和类型与备份所来自的存储卷相同(与
from_backup一起时不能给出size_bytes和shared:会抛出ValueError),位于备份所在的区域。它的status在完整拿到备份之前为restoring,之后为ready,在此之前不能挂载;from_backup指明来源备份。如果被分配到的机器无法恢复它,控制平面会在该区域的另一台机器上再创建一次:这时vol.info["restore_attempt"]为 2,vol.info["first_failure"]说明第一次为什么失败。如果这一次也失败,它就是failed(vol.info["failure"]),只能删除。 glac.usage(backup=...)、kind="backup"和group_by="backup"报告备份保存了多少,计在状态计量项上 (state_gb_hours);glac.billing()["meters"]["state"]["backups_gb_hours"]说明该计量项中有多少来自备份。
域名
项目的域名是你自己的一个名称,指向应用(和它的默认地址一样)或沙箱的某个端口(自定义域名)。域名属于控制平面。
| 调用 | 作用 |
|---|---|
glac.domains.create(domain, app=),或 (domain, sandbox=, port=),配合 proxied= |
注册这个名称,在 DNS 上能查到 domain.verification["records"] 中的某条记录(CNAME,它同时把域名指向这里;或 TXT 记录)之前为 pending_verification;项目中已有该域名时返回 409 domain_exists。 |
glac.domains.get(id_or_name), list(app=, sandbox=), delete(domain) |
查找一个;列出全部,或指向某个应用或沙箱的域名;删除一个。 |
domain.update(app=, sandbox=, port=, proxied=), domain.refresh(), domain.delete() |
指向别处;重新读取(status、certificate);删除。 |
domain = glac.domains.create("api.example.com", app="api")
print(domain.verification["records"])浏览器
尚未开放。浏览器还没有向账户开放:下面是开放之前 SDK 目前的样子。
会话是一个托管的 Chromium,由 Playwright、Puppeteer 或任何其他 Chrome DevTools Protocol (CDP) 客户端从外部驱动。有客户端连接时它保持运行,最后一个客户端离开 sleep_after_seconds 秒后进入休眠,页面会保留。CDP 客户端只需要它的 cdp_url:凭据就在地址的主机名中,所以 repr 中不包含它。
| 调用 | 作用 |
|---|---|
glac.browsers.create(kind=, name=, region=, sleep_after_seconds=, connection_idle_seconds=, delete_after_idle_seconds=, metadata=, profile=, proxy=) |
一个处于运行状态的新会话(kind:默认为 chromium-headless;chromium 有屏幕;lightpanda 是没有布局引擎的浏览器)。如果用已有会话的名称创建,会返回那个会话:它会被唤醒,页面都还在。profile:配置文件的名称,或 {"name", "save", "version"},在返回会话之前加载(见下文)。proxy:你自己的代理的 URL(见下文)。 |
glac.browsers.get(id_or_name), list(status=, kind=, name=) |
查找一个;列出,最新的在前(status:awake、asleep、unavailable 或 failed)。沙箱在运行前就失败的会话不算会话:创建会被拒绝,返回 503 session_failed。 |
session.update(...), session.sleep(), session.rotate_url(), session.delete(save_profile=None) |
修改它的时间设置、元数据或代理;立即让它休眠(已打开的连接保持,连接上的下一条消息会唤醒它);换新地址(旧地址会被拒绝,经由它们建立的连接会断开);结束它(用需要保存的配置文件创建的会话会先保存配置文件,除非 save_profile=False)。glac.browsers 的每个调用都接受会话对象、id 或名称。 |
session.cdp_url |
CDP 客户端连接的地址:Playwright 的 connect_over_cdp、Puppeteer 的 browserURL。 |
session.status, session.error |
awake、asleep、unavailable(它的节点无法访问)或 failed(创建得到响应之后它的沙箱失败了:比如浏览器停止且没有恢复);error 说明原因,即节点给出的 {"code", "message"},没有失败时为 None。browser.failed 事件也会说明这一点(它的数据类型为 EventBrowserFailed)。 |
session.connect(playwright=None), session.close() |
在装有 Playwright 时(pip install playwright,不需要自带浏览器),返回一个通过 CDP 连接的 Playwright 浏览器;close 会停止它启动的 Playwright。 |
session.live_url, session.live(interactive=True) |
在浏览器中实时观看其页面的地址:无头会话是随变化实时画出的标签页,有头会话是它的屏幕;观看者的鼠标和键盘操作会传到页面上。live(interactive=False) 给出一个单独的地址,任何操作都传不到页面,用来给人看,或放进你自己的页面。观看者和 CDP 客户端一样,会让会话保持运行。两个地址都会被 rotate_url 更换,读取它们需要密钥有 url 权限范围。 |
session.save_profile(name=None), session.profile |
立即保存它的配置文件,作为 name 的最新版本(不给出时为创建它时所用的配置文件);创建它时所用的或最近一次保存的配置文件 ({"name", "save", "version"})。 |
glac.browsers.profiles.list(), get(name), delete(name) |
项目的配置文件,每个都是一个 BrowserProfile,带有最新的 version、bytes、saved_at,以及它的全部 versions。 |
session.proxy, session.proxy_bytes |
它自己的代理 {"url", "username"}(从不包含密码),或为 None;双向经过代理的字节数(session.proxy_sent_bytes 和 proxy_received_bytes 分别是每个方向的)。 |
with glac.browsers.create(name="agent-42") as session: # deleted when the block ends
browser = session.connect()
page = browser.contexts[0].pages[0]
page.goto("https://example.com")
page.screenshot(path="shot.png")会话和任何实例一样,按它使用的方块资源计费:计算时的 CPU、运行时的内存和出站流量(价格与套餐)。session.awake_seconds 是它已运行的时长。
配置文件决定了浏览器在它访问过的网站眼中是谁:它的 Cookie、Local Storage、IndexedDB、Session Storage 和偏好设置,不包括缓存。平台为项目保存它,不单独计费,会话从不持有访问它的密钥。登录一次、保存,之后用这个配置文件创建的每个会话都已登录:
session = glac.browsers.create(profile={"name": "acme-login", "save": True}) # saved again when it is deleted
browser = session.connect()
# ... log in ...
session.save_profile() # the browser restarts, and a connected client reconnects
later = glac.browsers.create(profile="acme-login") # logged in保存时,浏览器会像正常关闭时那样停止,以便写出它持有的内容,然后重新启动;加载时,会在返回会话之前用这个配置文件启动会话的浏览器(1 MB 的配置文件需要几百毫秒)。同一个配置文件同一时间只能有一次保存:正在保存时的另一次保存会被拒绝,返回 409 profile_busy。删除的配置文件立即消失;它的归档在 30 天后删除。
在付费版上,会话的网页流量可以经由你自己的 HTTP 代理出站,代理需要接受 CONNECT(免费版上返回 403 proxy_not_on_plan)。会话到 80 和 443 端口的连接,经由浏览器旁边的一个中继通过代理发出,浏览器本身对代理一无所知;QUIC 在那里会被拒绝,所以浏览器改用基于 TCP 的 HTTP。凭据由平台保管,不会出现在任何响应中(session.proxy 只显示地址和用户名):
session = glac.browsers.create(name="agent-42", proxy="http://user:password@proxy.example.com:3128")
session.update(proxy="http://other:password@proxy.example.com:3128") # the browser goes on; its next connections go through this one
session.update(proxy=None) # none
print(session.refresh().proxy_bytes) # what went through it, both waysproxy 也接受 {"url": ...}。代理不建立隧道时,通过 HTTP 加载的页面会得到 502 proxy_failed(通过 HTTPS 的页面会在握手时失败)。经过代理的流量和其他流量一样算作会话的流量。参见自己的代理。
会话的 id 就是沙箱的 id:glac.sandboxes.get(session.id) 可以在里面运行命令。使用 Playwright 的异步 API 时,await p.chromium.connect_over_cdp(session.cdp_url) 的作用与 connect 相同。会话属于控制平面。
数据库
数据库是托管的 Postgres:运行在云盘上的独立 PostgreSQL,没有任何请求时休眠,有连接时被唤醒,会话都会保留。它大约两秒就能创建好,带有根分支 main 和所有者 glacdb_owner(glac_superuser 的成员,不是超级用户)。所有者的密码只出现在 create 和 reset_password 的响应中,任何地方都不保存:只有它们返回的数据库的 connection_string() 才带有密码。
import psycopg
db = glac.databases.create("shop") # ready, with the owner's password
with psycopg.connect(db.connection_string()) as conn: # from a sandbox or a service of the project
conn.execute("create table orders (id bigserial primary key, total numeric)")| 调用 | 作用 |
|---|---|
glac.databases.create(name, version=, region=, database_name=, size=, storage=, sleep_after_seconds=, suspend_after_seconds=, wake_daily_at=, metadata=, history_seconds=, tiering=) |
创建一个数据库并等待它第一次启动:ready,或 creating(反复 db.refresh(),直到就绪)。version:16、17 或 18(默认);size:{"vcpus", "memory_bytes"},即上限(默认 1 vCPU 和 1 GiB);storage:{"size_bytes", "durability"},它的云盘,strict(默认:对象存储收到提交后提交才返回)或 standard;sleep_after_seconds:1 到 3600(默认 1);suspend_after_seconds:保存内存、真正休眠之前保持暂停多久,暂停期间连接在一两毫秒内就能得到响应,1 到 86400(默认 300);wake_daily_at:休眠期间每天在 UTC 的哪个时刻 ("HH:MM") 唤醒它(它的 pg_cron 任务无论如何都会在各自的时间唤醒它)。用已有数据库的名称创建时,返回那个数据库,不带密码。glac.databases.update(db, suspend_after_seconds=, wake_daily_at=)(以及 db.update(...))修改这两项,所有分支一起修改;None 把其中任意一项恢复默认(分别为 300 和不唤醒)。免费版数据库的休眠设置是固定的,与 Neon 免费版缩容到零的做法相同:最后一个查询一秒后暂停(sleep_after_seconds 为 1),五分钟后休眠(suspend_after_seconds 为 300);无论创建还是修改,其他值都会以 400 invalid_field 被拒绝。 |
glac.databases.get(id_or_name), list(name=), delete(db, final_snapshot=True) |
查找一个;列出,最新的在前;删除一个数据库及其分支,除非 final_snapshot=False,否则会把它的云盘快照保留七天 (db.final_snapshot)。只有创建和重置的响应才带有密码。 |
glac.databases.branches(db), branch(db, "main"), sleep(db, "main"), wake(db, "main") |
它的分支(先是 main,然后是独立分支,最早的在前)、其中一个分支,以及立即让某个分支休眠或唤醒它(它的内存会被保存,醒来时会话都还在;连接也会唤醒它)。 |
glac.databases.create_branch(db, name, parent=, expires_at=), db.create_branch(name, ...); glac.databases.delete_branch(db, branch), db.delete_branch(branch), branch.delete() |
独立分支:它的父分支(默认 main,可用 parent 指定其他分支)此刻的状态,不复制任何数据,有自己的 PostgreSQL、端点和云盘,在项目网络上的地址是 <database>-<name>;给出 expires_at 时,到时会自行删除。它的角色和密码与父分支相同:创建它的 Database 带有所有者的密码时,分支的 connection_string() 也带有。删除会停止它的 PostgreSQL 并删除它的云盘;不能删除 main (409 root_branch),也不能删除有其他分支从它创建的分支 (409 has_branches)。 |
glac.databases.create(..., history_seconds=), update(db, history_seconds=), restore(db, at=, exact=), create_branch(db, name, at=, exact=); db.update(history_seconds=), db.restore(at=, exact=), db.history_seconds, db.history; create(..., tiering=), update(db, tiering=), db.info["tiering"] |
数据库的历史(slice DB8 和 DB5):根分支按时间最多能恢复到多久以前,或从多久以前创建分支:默认一天;付费版可设为 0(不保留)到 30 天,免费版为一天。恢复会把根分支原地带回过去:恢复到整分钟或某个版本自身的时刻时,使用云盘版本窗口中该时刻或之前最新的版本(每个有写入的 30 秒一个版本,最多保留一天);更早的时刻、精确到秒的时刻,或带 exact=True 时的那个精确时刻(精确到毫秒),则通过 WAL 归档恢复(把 WAL 重放到那一刻:需要几秒到几分钟,调用会一直等待)。端点、连接字符串和所有者的密码都保持不变,现有连接会断开,恢复之前的状态会作为一个独立分支 main_old_<the restore's moment> 保留;如果新版本在五分钟内不能提供服务,恢复会被撤销 (503 database_restore_failed)。按时间创建分支时,则以那个时刻创建分支(以 from: {at, exact} 发送)。历史中没有的时间返回 400 invalid_history,错误信息会说明版本窗口和归档分别能追溯到什么时候。精确到秒的时间(或带 exact)只能从归档恢复,否则拒绝,从不退而使用窗口中之前的版本:归档追溯不到时返回 400 invalid_history(数据库不保留归档,或时间早于最早的基础备份),归档的存储拒绝提供分段时返回 503 database_archive_unavailable(可重试);每次拒绝都会指明窗口中该时间或之前的版本,你可以改为请求那个版本自身的时刻。存储给不出所需数据块时,返回 503 drive_unreadable(可重试),不会创建任何东西。如果对象存储中断太久,等待归档的 WAL 超过了云盘为日志预留的空间,这部分 WAL 会被放弃,以保证数据库继续运行:这就是归档中的一个缺口,记录在 db.history["gaps"] 中(DatabaseHistoryGap:from、to、from_lsn、to_lsn):被放弃的 WAL 从何时开始,归档到何时为止无法恢复任何时间(缺口之后第一个基础备份完成的时间;在有基础备份之前为 null),以及缺口在日志中的起止位置。缺口内的时间只能从云盘的版本窗口恢复(整分钟,或某个版本自身的时刻);精确到秒或精确时刻的恢复返回 400 invalid_history。缺口也会以事件的形式报告:首次发现缺口时发出 database.history_gap(数据库运行期间,每个缺口一次),缺口之后归档重新可用于恢复时发出 database.history_resumed;它们的 data 是一个 EventDatabaseGap:数据库,缺口的 from、to(前者为 None)、from_lsn 和 to_lsn,segments(放弃了多少个 16 MB 的分段),以及前者才有的 error(对象存储最后一次拒绝时返回的内容)。db.history 给出当前的状况:from 是现在能恢复到的最早时间(第一个版本出现之前为 None,大约在数据库就绪三分钟后出现),以及 stored_bytes。tiering=True 或 False 用来开启或关闭分层存储(slice DB18:付费版可用;每个分支的 PostgreSQL 会为此重启一次),返回内容中的 tiering 说明是否已开启,以及目前按多少计费 (tiered_bytes)。 |
glac.databases.reset_password(db, role=) |
为它的所有者设置新密码,只出现在这次响应中:从下一次登录起旧密码会被拒绝,已打开的连接保持。 |
glac.databases.roles(db), reset_role_password(db, role); db.roles(), db.reset_role_password(role) |
根分支中可登录的角色,直接从 PostgreSQL 读取 ({"name", "owner"}):它的所有者和你创建的角色 (CREATE ROLE … LOGIN),平台自己的角色不在其中;以及为其中任何一个角色设置新密码,只出现在那次响应中:一个 DatabaseRoleSecret,带有该角色的 password 和根分支上该角色的连接字符串 (connection_string());所有者的连接字符串由 API 响应的数据库生成。独立分支保留它创建时的密码。 |
db.connection_string(password=, pooled=, private=) |
psycopg、psql 和 SQLAlchemy 的 postgresql+psycopg:// 都能直接使用的连接字符串:API 给出公网连接字符串时用公网的,否则用项目网络上的,并带有所有者的密码(来自与之一起返回的响应),或者 password(百分号编码)。 |
db.default_branch, branch.connection_uri, pooled_uri, private_uri, endpoint, state |
它的根分支,以及 API 给出的连接字符串;它的端点 (ep-<adjective>-<noun>-<8 hex>);awake、paused 或 asleep。db.refresh()、db.sleep()、db.wake()、db.branches()、db.reset_password()、db.delete() 都是数据库自己的方法。 |
数据库在项目网络上按名称访问,来自它的沙箱(用 project_network 创建)和服务:这就是它的 private_uri。有边缘节点提供 PostgreSQL 服务时,也可以从任何地方通过它的公网地址访问(connection_uri,端口 5432,只支持 TLS;连接池的地址:pooled_uri);没有边缘节点提供、或数据库没有可用来命名的区域时,它们为 None;给出时,连接字符串会使用它们。repr 和这里的其他任何东西都不会显示密码。数据库的应用和云盘在列出时带有 managed_by,只能通过 glac.databases 修改。数据库属于控制平面。
托管应用
目录中的应用(比如 Rivet)以它自己的方块资源安装到项目中:它的应用、存储卷、机密和存储桶,都带有该安装的标签,只能通过这个安装修改,并按这些资源本身计费(托管应用)。安装的响应包含全部输出,包括机密输出(管理员令牌、带有令牌的端点),之后的读取只对有 admin 权限范围的密钥显示这些输出:安装对象会保留之前的响应给出、而之后的响应隐藏的每个输出。
rivet = glac.catalogue.get("rivet") # its parameters, and a month of it in Cube's meters
print(rivet.calculator["month"])
engine = glac.installations.create("rivet", parameters={"disk_gib": 20}) # waits until it is ready
print(engine.outputs["endpoint"]) # RIVET_ENDPOINT, its token in it| 调用 | 作用 |
|---|---|
glac.catalogue.list(), glac.catalogue.get(app, version=) |
目录中的应用,每个都是一个 CatalogueApp(app、version、title、summary、publisher、verified、licence、versions、parameters:一个 JSON Schema,outputs、带有 month 的 calculator);查看一个应用的最新版本或指定的版本,以及它的 definition。 |
glac.installations.create(app, name=, version=, region=, parameters=, requires=, wait=True, timeout_seconds=) |
安装它,并等它就绪(wait=False:立即返回)。安装失败时抛出 InstallationError,附带错误信息和 installation;timeout_seconds 之后仍在创建时,抛出 GlacTimeoutError。 |
glac.installations.list(app=), glac.installations.get(id_or_name) |
项目的安装,最新的在前;查看一个。 |
installation.update(version=, parameters=, wait=True), installation.delete(), refresh(), wait(timeout_seconds=) |
换一个版本,或以合并补丁的方式修改参数(None 把参数恢复为默认值);连同它的所有组成部分一起删除(status:deleted 或 deleting)。 |
installation.status, failure, outputs, secret_outputs, parameters, apps, volumes, buckets, requires, required_by |
按 API 给出的样子。 |
托管应用属于控制平面。
机密与变量
glac.secrets.put("GHCR", "me:ghp_…", allowed_hosts=["ghcr.io"]) # never given back; goes to these hosts only
glac.secrets.update("GHCR", allowed_hosts=["ghcr.io", "*.example.com"])
glac.variables.put("REGION", "eu") # an app's env refers to it: "${{ shared.REGION }}"
print([s.name for s in glac.secrets.list()], {v.name: v.value for v in glac.variables.list()})
glac.secrets.delete("GHCR")机密只能通过名称识别,所以每个调用都接受名称或列出得到的 Secret,任何响应中都没有它的值。共享变量属于控制平面。
项目、账户与账单
| 调用 | 作用 |
|---|---|
glac.project(), glac.events(type=, sandbox=, limit=, cursor=) |
密钥所属的项目,以及它的事件的一页。 |
glac.usage(start=, end=, group_by=, sandbox=, metadata=, kind=, volume=, drive=, snapshot=, backup=, exact=) |
用量:按沙箱,或来自按小时账目的报告。 |
glac.account(), glac.update_account(...) |
账户、本月的流量、通知发往哪里(email=;email_verified,以及发往它的邮件被退回期间的 email_undeliverable,一个 EmailUndeliverable:参见通知发往哪里)、通知的语言(language=:“en”、“de” 或 “fr”,或 None 表示重新跟随账单地址)、账单地址和税号;以及在控制平面后面的 keeps(一个 AccountKeeps):密钥所属项目在云盘和存储卷上的保有量,按创建时所受限制的数字给出,每项都附带已有的量(volumes、volume_bytes、volumes_bytes,其 limit 为 None 表示不限,以及 unsized_bytes:参见提高限制)。 |
glac.billing(), glac.update_billing(plan=, spend_limit_usd=, start_now=) |
套餐和账单;设置套餐、支出上限。 |
glac.balance(), glac.update_balance(auto_reload=, ..., start_now=), glac.top_up(usd, start_now=), glac.charges(), glac.ledger() |
持有的点数、自动充值、充值、银行卡的扣款记录和账目。 |
Webhook、API 密钥、支出上限、项目。glac.webhooks 把项目的事件发到你的地址:create(url, events=, enabled=)(为每次投递签名的 secret 只出现在这次响应中)、get、list、update、delete。glac.keys 是项目的 API 密钥:create(name=, scopes=, expires_at=)(密钥本身 key 只出现在这次响应中)、get、list、delete。glac.limits() 和 glac.update_limits(usd) 是项目的支出上限和本月已支出的金额(None 去掉上限),sbx.limits() 和 sbx.update_limits(usd) 则是某个沙箱自己的。glac.request_limit(limit, wanted, why) 申请提高账户的某项可应要求提高的限制(按名称指定的上限、"volumes" 或 "volume_bytes";后两者中的大小以字节为单位;同一项限制已有申请在等待时返回 409 limit_request_pending),glac.limit_requests(limit=, cursor=) 和 glac.limit_request(id) 读取申请及运营方的答复(LimitRequest:status 为 pending、granted 或 refused,granted、answer)。glac.update_project(delete_after_idle_days=30) 设置沙箱休眠多少天后被删除(None:永不删除)。属于控制平面。Webhook 和 API 密钥需要有 admin 权限范围的密钥,设置项目的上限和 update_project 也一样;设置沙箱的上限需要 write。
日志导出目标、审计日志、计费文件。glac.destinations 是项目中应用的日志(以及指标和请求的追踪:signals=["logs", "traces"])导出的地方(OpenTelemetry 端点):create(name, endpoint=, headers=, ...) 创建时会立即用一条记录测试 (status["connected"]),还有 get、list、update、delete、test(立即发送一条记录);请求头的值是凭据,只在创建或修改导出目标时发送,不会出现在任何响应中(返回的内容只有请求头的名称)。glac.audit(limit=, cursor=) 是项目审计日志的一页,最新的在前(谁、什么方法、什么路径、什么状态):每一次不是读取的调用(被拒绝的也算),以及运营方对应用日志的每一次读取。glac.statement("2026-09") 是某个月的对账单(不给出时为本月;也可以给出该月中的某个 date),其中每个数据库单独一行(databases:它这个月的用量,其中历史和分层数据单列,以及按标价折合多少),glac.invoices() 是 Stripe 上的发票(附 PDF 链接),glac.notices() 是发给账户的邮件,glac.withdrawals() 是现在可以撤回的购买和已做的撤回(欧洲经济区或英国的消费者可以在 14 天内撤回一笔购买),glac.withdraw(charge_id, name=, email=) 是买方本人的撤回声明,立即受理。属于控制平面。创建、修改、测试和删除导出目标、读取审计日志以及撤回,需要有 admin 权限范围的密钥;其他调用只需要有读取权限的密钥。
每个调用都以 dict 返回 API 的响应;TypedDict(Account、Billing、Balance、Charge、Project、Event、EventAutoReloadNoRequest、EventInstanceRestarted、EventInstanceUpgraded、EventInstanceReverted、EventInstanceUpgradeFailed、EventLimitRequest 等)说明其中有什么。账单、余额和项目属于控制平面:独立运行的节点返回 404 no_such_route。
事件的数据。控制平面的每种事件,都有一个对应其 data (event["data"]) 的 TypedDict,以描述中该事件的 schema 命名。数据库的事件:EventDatabase(database.created,附带它的 region 和 PostgreSQL version;database.deleted,附带 final_snapshot_id,即保留七天的云盘快照)、EventDatabaseBranch(database.branch_created,附带它的 parent;database.branch_deleted,附带它的 reason)、EventDatabaseRestored(database.restored:时刻 at,以及 old_branch_id,即保存恢复之前状态的分支)、EventDatabaseArchive(database.archive_failing:since、连续失败的分段数 failures、archived_until,即在此期间恢复能追溯多远,以及对象存储的 error;database.archive_recovered:recovered_at)和 EventDatabaseGap。其他事件也一样:沙箱的(EventSandboxSlept、EventSandboxWoke、EventSandboxMoved、EventSandboxDeleted 等)、应用及其键的(EventApp、EventAppFailed、EventKeyCreated 等)、项目的部署和密钥、备份、域名、导出目标和云盘快照的(EventProjectDeployed、EventBackup、EventDomain、EventDestination、EventDriveSnapshot 等),以及账户的套餐、充值、退款、撤回、余额、点数、存储、出站流量上限、支出上限、冻结和欠款的(EventSubscriptionPeriod、EventTopupCollected、EventWithdrawalReceived、EventBalance、EventInDebt 等)。某类事件并非都有的键,在它的类型中是可省略的键。
立即购买 (start_now)。欧洲经济区或英国的消费者(没有已验证或正在验证的税号的账户)可以在 14 天内撤回一笔购买,所购内容只有在其本人明确要求时才会立即提供。top_up(20, start_now=True)、update_billing(plan="paid", start_now=True)(从免费版购买付费版)和 update_balance(auto_reload=True, start_now=True)(自动充值保持开启期间的每一次自动充值)都带有这一要求,措辞与控制台在勾选框旁显示的相同(GET /v1/billing/withdrawals 中也有,见 right.start_now):
我要求在可以撤回这笔购买的 14 天内立即提供所购内容。如果我撤回,我需要为此前已提供的部分付费。我理解,一次充值的点数全部用完后,即视为已全部提供,我将不能再撤回。
服务立即开始;在 14 天内,买方仍可撤回,并为此前已提供的部分付费;一旦全部提供(一次充值的点数全部用完),撤回权即丧失。只有在购买者本人提出这一要求时才加上它:SDK 不会自行发送它。没有它时,这类账户的购买会被拒绝:返回 400 start_now_required,param 为 start_now,错误信息会指明账户所在的国家并引用这一要求的原文,什么也不会买。开启了自动充值却没有这一要求的账户不会自动充值,并且每月会收到一次通知(auto_reload.no_request 事件)。
错误
非零退出码是一个结果:读取 res.exit_code 即可。API 的错误会抛出 GlacError,无论调用属于哪个产品,带有 status、type、code、param、retryable、request_id 和 message,以及指向错误页面上对应错误码的 docs_url。连不上时抛出 GlacConnectionError;超时抛出 GlacTimeoutError(它也是 TimeoutError)。没能起来的版本或失败的项目部署抛出 DeployError,放弃读取日志时抛出 LogsError;两者都是 GlacError。与费用有关的错误(402 account_in_debt、403 trust_limit_exceeded、409 no_card、400 start_now_required 等)带有控制平面自己的错误信息,说明怎样解除。
值得了解的行为
- 休眠的沙箱。不会有调用因为沙箱在休眠或正在启动而失败:它会等待,然后继续。没有专门的唤醒调用。
run()和wait()算作等待,所以它们等待期间沙箱保持运行;读取cmd.output()则不算。 - 重试。没有响应的调用,或出现
retryable错误时,会重试两次 (retries=),间隔从 0.2 秒开始翻倍。每个POST都带有Idempotency-Key,所以重试永远不会创建两个沙箱,也不会把一条命令运行两次。 - 超时。
Glac(timeout_seconds=)(默认 900 秒)是等待 API 的时长,不包括命令自己的运行时间。run(timeout_seconds=)是命令的时限:超时后命令被终止,结果显示timed_out。 - 线程。
Glac、Sandbox和Command都可以在多个线程中使用。目前还没有 asyncio API:请用await asyncio.to_thread(sbx.run, "make")。