diff --git a/deploy/node/README.md b/deploy/node/README.md index 18254f1cf..79c5edac3 100644 --- a/deploy/node/README.md +++ b/deploy/node/README.md @@ -21,8 +21,8 @@ The node installer runs as root and prepares the host for one node per installat - It never installs Docker, KVM or packages and never changes device permissions. It refuses SELinux-enforcing hosts and changes nothing when a check fails. The enrollment token comes only on standard input, never in arguments or the environment. - Files the service account owns are read, written and deleted only with that account's credentials. The one exception is root removing the account's home after `userdel`, when no process can still run as that account. That work runs in a child that starts its own session with `/dev/null` as input, joins a new session keyring and dies with its parent; root shows its output only as plain text (terminal controls become `?`). SIGINT, SIGHUP and SIGTERM stop that child and what it started. - Root never runs a file the service account can write, opens a URL it wrote, or follows a link in its home. Capture the trusted bootstrap bytes before dropping to the service account and pass them through the fork; the service account writes its own retained generation helper. Never open the caller's private download directory to it or let root write into service-owned state. -- The generated bootstrap passes only the six standard HTTP/HTTPS proxy and bypass variables through sudo and gives both spellings the lowercase value when present, even if empty, so curl, urllib and the Go registration command follow the same rules. The installation child keeps just those names beside its fixed environment. Proxy values stay out of arguments, saved configuration, service units and diagnostics; never use broad sudo environment inheritance. This covers installation downloads only, not the node service. -- `--uninstall` removes a node only after Core rejects its credential, except for a node that never registered and with `--force`, which Web offers when the old Core address no longer responds. It never touches sandboxes, volumes or images (the Runtime image and the microsandbox store stay), deletes the account only when the installer created it and no node remains, and otherwise removes only the groups it added. +- The generated bootstrap passes only the six standard HTTP/HTTPS proxy and bypass variables through sudo and gives both spellings the lowercase value when present, even if empty, so curl, urllib and the Go registration command follow the same rules. The installation child keeps just those names beside its fixed environment. Proxy values stay out of arguments, unit files and diagnostics; never use broad sudo environment inheritance. The installation child uses them for downloads. The node service keeps the settings of the latest run in `/etc/oac-node/.env` (root, mode 0600, read through `EnvironmentFile=`; removed when a run has no proxy and by uninstall), with loopback added to `NO_PROXY`. +- `--uninstall` removes a node only after Core rejects its credential, except for a node that never registered and with `--force`, which Web offers when the old Core address no longer responds. It never touches sandboxes, volumes or Docker images (the Runtime image stays, with the command that removes it). Once Core confirmed the removal it also deletes the node's microsandbox store, which `--force` never does (it prints the command instead). It deletes the account only when the installer created it and no node remains, and otherwise removes only the groups it added. - Refuse resources of an older product name for the same installation ID; never adopt them or remove another installation's resources. ## Download contract diff --git a/deploy/node/node_install.py b/deploy/node/node_install.py index 6777a33dc..790d555a1 100644 --- a/deploy/node/node_install.py +++ b/deploy/node/node_install.py @@ -759,11 +759,7 @@ def service_child(account, function, arguments, parent, mask, read_ends, output_ # service. Keep only standard HTTP proxy settings across the UID boundary. # curl/urllib prefer lowercase, while the Go registration command prefers # uppercase. Give both spellings one value, including an explicit empty value. - proxies = {} - for name in ("http_proxy", "https_proxy", "no_proxy"): - value = os.environ.get(name, os.environ.get(name.upper())) - if value is not None: - proxies[name] = proxies[name.upper()] = value + proxies = proxy_settings(os.environ) os.environ.clear() os.environ.update(HOME=account.pw_dir, USER=account.pw_name, LOGNAME=account.pw_name, PATH=SAFE_PATH, LANG="C.UTF-8", DOCKER_CONFIG=str(CHILD_DOCKER_CONFIG)) @@ -790,6 +786,48 @@ def service_child(account, function, arguments, parent, mask, read_ends, output_ run_as = as_service_user +def proxy_settings(environment): + """The standard proxy variables, each under both spellings with one value (an explicit empty value included).""" + settings = {} + for name in ("http_proxy", "https_proxy", "no_proxy"): + value = environment.get(name, environment.get(name.upper())) + if value is not None: + settings[name] = settings[name.upper()] = value + return settings + + +def service_environment(environment): + """The proxy settings the node service keeps, as an EnvironmentFile; empty without an HTTP(S) proxy. + + Loopback always stays direct. Systemd reads quoted values without expanding anything.""" + settings = proxy_settings(environment) + if not (settings.get("http_proxy") or settings.get("https_proxy")): + return "" + direct = "localhost,127.0.0.1,::1" + for name in ("no_proxy", "NO_PROXY"): + settings[name] = ",".join(filter(None, (settings.get(name, "").strip(), direct))) + if any(set(value) & {"\n", "\r", "\0"} for value in settings.values()): + raise InstallError("A proxy setting contains a line break; fix it, then rerun the command." + NOTHING_CHANGED) + return "".join(name + '="' + value.replace("\\", "\\\\").replace('"', '\\"') + '"\n' for name, value in sorted(settings.items()) if value) + + +def write_service_environment(path, content): + """Keep the installer's proxy settings in a root-only file the unit reads; True when they changed.""" + if path.is_symlink() or (path.exists() and not path.is_file()): + raise InstallError(str(path) + " must be a regular file") + current = path.read_text() if path.exists() else "" + if current == content: + return False + if not content: + path.unlink() + return True + with distribution.temporary_file(path) as temporary: + with os.fdopen(os.open(temporary, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600), "w") as stream: + stream.write(content) + os.replace(temporary, path) + return True + + def child_docker_config(): """Create the root-owned empty directory the service user's docker calls read.""" path = CHILD_DOCKER_CONFIG @@ -1064,7 +1102,7 @@ def prepare_account(account, record, group): return service_account() -def system_unit(root, provider): +def system_unit(root, provider, environment): def quote(value): return '"' + str(value).replace("\\", "\\\\").replace('"', '\\"').replace("%", "%%") + '"' after = "network-online.target docker.service" if provider == "docker" else "network-online.target" @@ -1074,6 +1112,7 @@ def quote(value): + "\nStartLimitIntervalSec=0\n\n[Service]\nType=exec\nUser=" + SERVICE_USER + "\nExecStart=:" + quote(root / provider_assets.artifacts(provider, ("node",))[0]) + " run --config " + quote(root / "provider.json") + " --state-dir " + quote(root / "state/node") + "\nWorkingDirectory=" + str(root).replace("%", "%%") + + "\nEnvironmentFile=-" + str(environment).replace("%", "%%") + "\nRestart=on-failure\nRestartSec=5s\nRestartPreventExitStatus=78\nKillMode=process\nUMask=0077" + "\n\n[Install]\nWantedBy=multi-user.target\n") @@ -1105,6 +1144,7 @@ def install_system(args, token): host_checks() # Checks that change nothing run first, so a refusal leaves no trace, not even a lock. record = node_record(args.installation_id) + proxy = service_environment(os.environ) configuration = None if record is None: install_display.step("Reading the Core deployment specification") @@ -1136,9 +1176,13 @@ def install_system(args, token): "core_url": args.core_url, "node_root": str(root), "unit": str(unit)})) args.system, args.provider = True, provider run_as(account, prepare_service_node, args, token, helper_archive) - root_file(unit, system_unit(root, provider)) + environment = SYSTEM_RECORDS / (args.installation_id + ".env") + root_file(unit, system_unit(root, provider, environment)) + changed = write_service_environment(environment, proxy) install_display.step("Starting the node service") checked(["systemctl", "daemon-reload"], "Cannot reload systemd") + if changed: # A running service reads its environment only when it starts; a stopped one is left to enable. + checked(["systemctl", "try-restart", unit.name], "Cannot restart the node service with the new proxy settings. Inspect sudo journalctl -u " + unit.name) checked(["systemctl", "enable", "--now", unit.name], "Cannot start the node service; its identity is kept. Inspect sudo journalctl -u " + unit.name) checked(["systemctl", "is-active", "--quiet", unit.name], "Node service is unavailable; inspect sudo journalctl -u " + unit.name) install_display.step("Waiting for Core connection and provider readiness") @@ -1226,8 +1270,8 @@ def release_docker_network(installation_id): checked(list(DOCKER) + ["network", "rm", network], "Cannot remove Docker network " + network) -def remove_node_files(root, installation_id): - """Delete the node's state directory; keep the Runtime image and the microsandbox store. +def remove_node_files(root, installation_id, delete_store): + """Delete the node's state directory and, once Core confirmed the removal, its microsandbox store; keep the Runtime image. Runs as the node's own user, so a link it planted can never reach another user's files.""" no_links(root) @@ -1240,11 +1284,14 @@ def remove_node_files(root, installation_id): if isinstance(image, str) and re.fullmatch(r"sha256:[0-9a-f]{64}", image): print("Kept the Runtime image " + image + "; remove it with `sudo docker image rm " + image + "` if no other node uses it.") if runtime_home.exists(): - sudo_mode = str(Path.home()) == str(SERVICE_HOME) - remove = ("sudo -u " + SERVICE_USER + " rm -rf " if sudo_mode else "rm -rf ") + str(runtime_home) - print("Kept the microsandbox store " + str(runtime_home) + " with its images and any sandbox state. When no " - "microVM runs (`pgrep -u " + os.environ.get("USER", "") + "` is empty), remove it with `" + remove - + "`, then rerun this uninstall command.") + no_links(runtime_home) + if delete_store and private_json(runtime_home / "oac-installation.json") == {"installation_id": installation_id}: + shutil.rmtree(runtime_home) + print("Deleted the microsandbox store " + str(runtime_home) + ".") + else: + sudo_mode = str(Path.home()) == str(SERVICE_HOME) + print("Kept the microsandbox store " + str(runtime_home) + " with its images and any sandbox state. Delete it with `" + + ("sudo -u " + SERVICE_USER + " rm -rf " if sudo_mode else "rm -rf ") + str(runtime_home) + "`.") def uninstall_system(args): @@ -1285,8 +1332,10 @@ def uninstall_system(args): with contextlib.suppress(InstallError): checked(["systemctl", "reset-failed", unit.name], "Cannot reset " + unit.name) release_docker_network(args.installation_id) - run_as(account, remove_node_files, root, args.installation_id) + # --force skips Core's confirmation, so it never deletes the store. + run_as(account, remove_node_files, root, args.installation_id, not args.force) (SYSTEM_RECORDS / (args.installation_id + ".json")).unlink(missing_ok=True) + (SYSTEM_RECORDS / (args.installation_id + ".env")).unlink(missing_ok=True) print("Node for installation " + args.installation_id + " uninstalled from this host.") release_account(account, account_record) diff --git a/deploy/node/test_node_install.py b/deploy/node/test_node_install.py index 0d52ec773..14d092818 100644 --- a/deploy/node/test_node_install.py +++ b/deploy/node/test_node_install.py @@ -871,6 +871,62 @@ def test_uninstall_waits_for_core_to_reject_the_node(self): self.assertFalse((system / "units" / ("oac-node-" + self.args.installation_id + ".service")).exists()) self.assertFalse((system / "etc").exists()) + def uninstall_output(self, force): + output = io.StringIO() + rejected = urllib.error.HTTPError("https://core.example", 401, "", {}, None) + with mock.patch.object(installer, "open_request", side_effect=rejected), mock.patch.object(installer.sys, "stdout", output): + installer.uninstall_system(SimpleNamespace(installation_id=self.args.installation_id, force=force)) + return output.getvalue() + + def test_uninstall_deletes_the_store_once_core_confirmed_the_removal(self): + self.sudo_host() + installer.install_system(self.args, "synthetic-once-token") + store = self.home / "m" + store.mkdir() + (store / "oac-installation.json").write_text(json.dumps({"installation_id": self.args.installation_id})) + (store / "image").write_text("layers") + output = self.uninstall_output(False) + self.assertFalse(store.exists()) + self.assertIn("Deleted the microsandbox store " + str(store), output) + self.assertNotIn("Kept the microsandbox store", output) + + def test_forced_uninstall_keeps_the_store_and_prints_the_command(self): + self.sudo_host() + installer.install_system(self.args, "synthetic-once-token") + store = self.home / "m" + store.mkdir() + (store / "oac-installation.json").write_text(json.dumps({"installation_id": self.args.installation_id})) + output = self.uninstall_output(True) + self.assertTrue(store.is_dir()) + self.assertIn("Kept the microsandbox store " + str(store), output) + self.assertIn("rm -rf " + str(store) + "`", output) + + def test_rerun_updates_the_service_proxy_and_restarts_the_service(self): + system = self.sudo_host() + unit = "oac-node-" + self.args.installation_id + ".service" + environment = system / "etc" / (self.args.installation_id + ".env") + with mock.patch.dict(os.environ, {"https_proxy": "http://proxy-one.invalid:3128", "http_proxy": ""}): + installer.install_system(self.args, "synthetic-once-token") + self.assertIn("proxy-one", environment.read_text()) + self.assertEqual(stat.S_IMODE(environment.stat().st_mode), 0o600) + self.assertNotIn("proxy-one", (system / "units" / unit).read_text()) + self.calls.clear() + with mock.patch.dict(os.environ, {"https_proxy": "http://proxy-two.invalid:3128", "http_proxy": ""}): + installer.install_system(self.args, "") + self.assertIn("proxy-two", environment.read_text()) + self.assertNotIn("proxy-one", environment.read_text()) + self.assertIn(["systemctl", "try-restart", unit], [call for call, _ in self.calls]) + self.uninstall_output(False) + self.assertFalse(environment.exists()) + + def test_uninstall_keeps_a_store_it_did_not_create(self): + self.sudo_host() + installer.install_system(self.args, "synthetic-once-token") + store = self.home / "m" + store.mkdir() + self.assertIn("Kept the microsandbox store", self.uninstall_output(False)) + self.assertTrue(store.is_dir()) + def test_uninstall_leaves_an_adopted_account_as_found(self): system = self.sudo_host() (self.home / "service").mkdir() @@ -958,13 +1014,13 @@ def test_uninstall_prints_only_an_image_id_from_the_service_home(self): (root / "provider.json").write_text(json.dumps({"native": {"image": "x\nFinish with: sudo sh"}})) output = io.StringIO() with mock.patch.object(installer.sys, "stdout", output): - installer.remove_node_files(root, self.args.installation_id) + installer.remove_node_files(root, self.args.installation_id, False) self.assertNotIn("Finish", output.getvalue()) root.mkdir() (root / "provider.json").write_text(json.dumps({"native": {"image": "sha256:" + "b" * 64}})) output = io.StringIO() with mock.patch.object(installer.sys, "stdout", output): - installer.remove_node_files(root, self.args.installation_id) + installer.remove_node_files(root, self.args.installation_id, False) self.assertIn("`sudo docker image rm sha256:", output.getvalue()) def test_service_user_step_gets_its_own_session_keyring(self): diff --git a/deploy/node/test_node_proxy.py b/deploy/node/test_node_proxy.py index 2f66ef52d..ef33b05a3 100644 --- a/deploy/node/test_node_proxy.py +++ b/deploy/node/test_node_proxy.py @@ -8,6 +8,7 @@ import pwd import select import socket +import stat import ssl import subprocess import sys @@ -193,11 +194,27 @@ def test_rejected_proxy_does_not_reveal_credentials(self): "http://downloads.invalid", expect_failure=True) self.assertIn("HTTP 407", error) - def test_installer_proxy_is_not_in_service_unit(self): - with mock.patch.dict(os.environ, {"https_proxy": "http://fixture:private%20password@proxy.invalid"}): - unit = installer.system_unit(Path("/var/lib/oac-node/.oac/nodes/fixture"), "docker") - self.assertIn("User=oac-node", unit) - self.assertNotIn("proxy", unit.lower()) + def test_service_keeps_the_installer_proxy_in_a_private_file(self): + secret = "http://fixture:private%20password@proxy.invalid:3128" + content = installer.service_environment({"https_proxy": secret, "NO_PROXY": "internal.invalid"}) + self.assertIn('https_proxy="' + secret + '"\n', content) + self.assertIn('HTTPS_PROXY="' + secret + '"\n', content) + self.assertIn('no_proxy="internal.invalid,localhost,127.0.0.1,::1"\n', content) + self.assertEqual(installer.service_environment({"no_proxy": "internal.invalid"}), "") + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) / "node.env" + unit = installer.system_unit(Path("/var/lib/oac-node/.oac/nodes/fixture"), "docker", path) + self.assertIn("EnvironmentFile=-" + str(path) + "\n", unit) + self.assertNotIn("private", unit) + self.assertTrue(installer.write_service_environment(path, content)) + self.assertEqual(stat.S_IMODE(path.stat().st_mode), 0o600) + self.assertFalse(installer.write_service_environment(path, content)) + # A rerun with other settings replaces them; one without a proxy removes the file. + other = installer.service_environment({"HTTP_PROXY": "http://other.invalid:3128"}) + self.assertTrue(installer.write_service_environment(path, other)) + self.assertNotIn("private", path.read_text()) + self.assertTrue(installer.write_service_environment(path, "")) + self.assertFalse(path.exists()) if __name__ == "__main__": diff --git a/docs/getting-started/nodes.md b/docs/getting-started/nodes.md index b49697b8f..248db3916 100644 --- a/docs/getting-started/nodes.md +++ b/docs/getting-started/nodes.md @@ -56,7 +56,7 @@ Export `http_proxy`, `https_proxy` and `no_proxy` before running the generated c The command asks sudo to preserve only these six names, and the installer keeps them when it drops to `oac-node`. If sudo policy refuses, ask the host administrator to permit those names, or run from a root shell with the variables exported there. An outer `sudo` you add yourself can discard them. Don't put credentials in command arguments or share shell traces. -The proxy applies only to installation downloads. It is not saved in the node's configuration or service, so the running node needs its own network access to Core. +The node service keeps the proxy settings of the latest run, so it reaches Core through the proxy it was installed with. They are stored in `/etc/oac-node/.env`, readable only by root, never in the unit file. Loopback addresses always stay direct. Rerunning the command with different settings replaces them and restarts the service; a rerun without a proxy removes them. ## What the installer sets up @@ -66,7 +66,7 @@ The proxy applies only to installation downloads. It is not saved in the node's | Group | The group that owns `/var/run/docker.sock` (Docker) or `/dev/kvm` (microsandbox): `docker` or `kvm`, nothing else | | Service | `/etc/systemd/system/oac-node-.service`, a root-owned system unit with `User=oac-node`, enabled at boot. It restarts every 5 seconds while Core is unreachable and stops for good once Core no longer accepts the node | | Node state | `/var/lib/oac-node/.oac/nodes//`: identity, configuration, node files. microsandbox keeps its images and sandboxes under `/var/lib/oac-node/.oac/m/` | -| Records | `/etc/oac-node/`: what the installer created or changed, used by reruns and uninstall | +| Records | `/etc/oac-node/`: what the installer created or changed, used by reruns and uninstall, and the service's proxy settings | | Docker | The Runtime image, imported once, and a network `oac-node-` | | microsandbox network policy | Sandboxes may reach Core, DNS on the host and public addresses, and nothing else: no inbound connections and no private networks | @@ -123,9 +123,9 @@ The [reset contract](../../contracts/agents-api/sandbox-deployment.md#generation $s python3 "$d/node-install.pyz" ${NO_COLOR+--no-color} --uninstall --installation-id '') ``` -Uninstall first asks Core, at the address the node enrolled with, whether the node was removed, and refuses while Core still lists it. When the node enrolled with an address other than the current public URL, the dialog also shows **Old Core address gone?**: if that address no longer responds, it gives the command with `--force`, which skips the check; remove the node on the Nodes page first. Without the dialog, take the installer's SHA-256 from the `node-install.pyz` line of `https://core.example/node-install/SHA256SUMS`. Uninstall stops and removes the service, the node state, the records and the Docker network. It deletes the `oac-node` account only if the installer created it and no node remains; an adopted account only loses the groups the installer added. +Uninstall first asks Core, at the address the node enrolled with, whether the node was removed, and refuses while Core still lists it. When the node enrolled with an address other than the current public URL, the dialog also shows **Old Core address gone?**: if that address no longer responds, it gives the command with `--force`, which skips the check; remove the node on the Nodes page first. Without the dialog, take the installer's SHA-256 from the `node-install.pyz` line of `https://core.example/node-install/SHA256SUMS`. Uninstall stops and removes the service, the node state, the records, the proxy settings and the Docker network. It deletes the `oac-node` account only if the installer created it and no node remains; an adopted account only loses the groups the installer added. -It never deletes sandboxes, volumes or images. It keeps the Runtime image and prints the `docker image rm` command. For microsandbox it keeps the store under `/var/lib/oac-node/.oac/m/`, prints how to delete it (`sudo -u oac-node rm -rf `), and keeps a created account until the store is gone; rerun uninstall afterwards. With `--force`, microVMs may still use the store, so check `pgrep -u oac-node` first. Uninstall can be rerun until it completes. +It never deletes Docker images or volumes. It keeps the Runtime image and prints the `sudo docker image rm` command. For microsandbox, once Core has confirmed the removal, it also deletes the node's store under `/var/lib/oac-node/.oac/m/` (about 1.5 GB), and with it the created account. With `--force` Core isn't asked, so the store is kept and the output prints the command that deletes it (`sudo -u oac-node rm -rf `); microVMs may still use it, so check `pgrep -u oac-node` first, then rerun uninstall to finish removing the account. The output lists what was deleted and what was kept. Uninstall can be rerun until it completes. ## Register a node manually @@ -192,7 +192,7 @@ A registered node that can't connect to Core says why in its [log](#logs): `sand | --- | --- | | `HTTP 400 on the WebSocket upgrade` | The reverse proxy in front of Core doesn't pass WebSocket upgrades on `/api/v1`; see [HTTPS and the reverse proxy](./install-options.md#https-and-the-reverse-proxy) | | Another status, such as `HTTP 404 on the WebSocket upgrade` or `HTTP 502 …` | The reverse proxy doesn't route `/api/v1` to Core or doesn't preserve Host, or Core is not running | -| `dial tcp: …`, such as `connection refused`, `i/o timeout` or `no such host` | The host can't reach Core at the public URL. Check DNS and firewalls; the node service doesn't use the installer's [download proxy](#download-through-a-proxy) | +| `dial tcp: …`, such as `connection refused`, `i/o timeout` or `no such host` | The host can't reach Core at the public URL. Check DNS and firewalls; the node service uses the [proxy](#download-through-a-proxy) the installer ran with, so rerun the command with the right settings if the proxy changed | | `tls: …`, or `TLS certificate expired or not yet valid` | The host doesn't trust Core's certificate, or the public URL uses `https` on a port that doesn't serve it | | `Core did not accept the node: …` | Core closed the connection after the node introduced itself. After a Core upgrade, apply the release's [protocol changes](./operations.md#protocol-changes); otherwise read Core's log | diff --git a/docs/zh/getting-started/nodes.md b/docs/zh/getting-started/nodes.md index 3e98f4309..0b91f2383 100644 --- a/docs/zh/getting-started/nodes.md +++ b/docs/zh/getting-started/nodes.md @@ -1,7 +1,7 @@ --- title: "添加和管理节点" source: docs/getting-started/nodes.md -source_hash: da770b5195c28cc9ffa6e2d374113adfb2b30a3526710386ff7c49ce8ac62a94 +source_hash: 68864862ec5696e17779a10923f326b5c2b9ba8d729cc1e30dad62c6415ee85a --- 节点是一台 Linux 主机,在沙箱提供商为 microsandbox(推荐)或 Docker 时,为 Core 托管 Session 运行沙箱。Core 将新 Session 分配给有空余容量的节点;节点创建沙箱,沙箱回连 Core。E2B 不需要节点。应用为自己的 Session 连接的机器是[自托管执行器](self-hosted.md),而不是节点。 @@ -58,7 +58,7 @@ Microsandbox 的检查点执行兼容性包含主机内核和 CPU profile。因 命令要求 sudo 仅保留这六个变量名,安装程序降权到 `oac-node` 时继续保留它们。如果 sudo 策略拒绝,请让主机管理员允许这些变量,或在 root shell 中导出变量后执行。你自行添加的外层 `sudo` 可能丢弃它们。不要将凭据放进命令参数,也不要分享 shell 跟踪输出。 -代理仅用于安装下载,不保存到节点配置或服务中,因此运行中的节点仍需独立访问 Core。 +节点服务保留最近一次运行所用的代理设置,因此通过安装时的代理访问 Core。设置保存在 `/etc/oac-node/.env`,仅 root 可读,不写入单元文件。回环地址始终直连。使用不同设置重新执行命令会替换它们并重启服务;不带代理重新执行则移除它们。 ## 安装程序设置的内容 {#what-the-installer-sets-up} @@ -68,7 +68,7 @@ Microsandbox 的检查点执行兼容性包含主机内核和 CPU profile。因 | 用户组 | 拥有 `/var/run/docker.sock`(Docker)或 `/dev/kvm`(microsandbox)的组:仅 `docker` 或 `kvm` | | 服务 | `/etc/systemd/system/oac-node-.service`,root 所属的系统单元,使用 `User=oac-node`,开机启用。Core 无法访问时每 5 秒重启;Core 不再接受节点后永久停止 | | 节点状态 | `/var/lib/oac-node/.oac/nodes//`:身份、配置和节点文件。microsandbox 在 `/var/lib/oac-node/.oac/m/` 保存镜像和沙箱 | -| 记录 | `/etc/oac-node/`:安装程序创建或修改的内容,用于重新运行和卸载 | +| 记录 | `/etc/oac-node/`:安装程序创建或修改的内容,用于重新运行和卸载,以及服务的代理设置 | | Docker | 一次性导入 Runtime 镜像,以及网络 `oac-node-` | | microsandbox 网络策略 | 沙箱仅能访问 Core、主机上的 DNS 和公开地址:无入站连接、不可访问私有网络 | @@ -125,9 +125,9 @@ root 只准备账号、组和服务单元;其他操作(包括 Docker 网络 $s python3 "$d/node-install.pyz" ${NO_COLOR+--no-color} --uninstall --installation-id '') ``` -卸载先向节点注册时使用的 Core 地址确认节点是否已移除;Core 仍列出该节点时拒绝卸载。注册地址与当前公开 URL 不同时,对话框还展示 **旧 Core 地址已失效?**:该地址不再响应时,提供带 `--force` 的命令以跳过检查;先在 **节点** 页面移除节点。不使用对话框时,从 `https://core.example/node-install/SHA256SUMS` 的 `node-install.pyz` 行获取安装程序 SHA-256。卸载停止并移除服务、节点状态、记录和 Docker 网络。只有安装程序创建了 `oac-node` 且不再有节点时,才删除该账号;复用的账号仅移除安装程序添加的组。 +卸载先向节点注册时使用的 Core 地址确认节点是否已移除;Core 仍列出该节点时拒绝卸载。注册地址与当前公开 URL 不同时,对话框还展示 **旧 Core 地址已失效?**:该地址不再响应时,提供带 `--force` 的命令以跳过检查;先在 **节点** 页面移除节点。不使用对话框时,从 `https://core.example/node-install/SHA256SUMS` 的 `node-install.pyz` 行获取安装程序 SHA-256。卸载停止并移除服务、节点状态、记录、代理设置和 Docker 网络。只有安装程序创建了 `oac-node` 且不再有节点时,才删除该账号;复用的账号仅移除安装程序添加的组。 -程序不删除沙箱、卷或镜像。保留 Runtime 镜像并输出 `docker image rm` 命令。microsandbox 保留 `/var/lib/oac-node/.oac/m/` 存储,并输出删除方法(`sudo -u oac-node rm -rf `);存储删除前保留所创建的账号,之后重新卸载。使用 `--force` 时 microVM 可能仍使用存储,请先检查 `pgrep -u oac-node`。可以重复卸载直到完成。 +程序不删除 Docker 镜像或卷。保留 Runtime 镜像并输出 `sudo docker image rm` 命令。microsandbox 在 Core 确认节点已移除后,还会删除节点位于 `/var/lib/oac-node/.oac/m/` 的存储(约 1.5 GB),随后删除所创建的账号。使用 `--force` 时不询问 Core,因此保留存储,并输出删除命令(`sudo -u oac-node rm -rf `);microVM 可能仍使用存储,请先检查 `pgrep -u oac-node`,再重新卸载以完成账号移除。输出会列出已删除和已保留的内容。可以重复卸载直到完成。 ## 手动注册节点 {#register-a-node-manually} @@ -194,7 +194,7 @@ root 只准备账号、组和服务单元;其他操作(包括 Docker 网络 | --- | --- | | `HTTP 400 on the WebSocket upgrade` | Core 前的反向代理没有在 `/api/v1` 上传递 WebSocket 升级;参见 [HTTPS 与反向代理](install-options.md#https-and-the-reverse-proxy) | | 其他状态码,例如 `HTTP 404 on the WebSocket upgrade` 或 `HTTP 502 …` | 反向代理没有将 `/api/v1` 转发到 Core 或没有保留 Host,或者 Core 未运行 | -| `dial tcp: …`,例如 `connection refused`、`i/o timeout` 或 `no such host` | 主机无法通过公开 URL 访问 Core。检查 DNS 和防火墙;节点服务不使用安装程序的[下载代理](#download-through-a-proxy) | +| `dial tcp: …`,例如 `connection refused`、`i/o timeout` 或 `no such host` | 主机无法通过公开 URL 访问 Core。检查 DNS 和防火墙;节点服务使用安装程序运行时的[代理](#download-through-a-proxy),代理变化后请用正确的设置重新执行命令 | | `tls: …`,或 `TLS certificate expired or not yet valid` | 主机不信任 Core 的证书,或公开 URL 对不提供 HTTPS 的端口使用了 `https` | | `Core did not accept the node: …` | 节点自我介绍后 Core 关闭了连接。Core 升级后,执行该发行版的[协议变更](operations.md#protocol-changes);否则查看 Core 日志 |