@crvouga/dev-fleet 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.mise/locks/aws-cdk/2.1146.1/aube-lock.yaml +24 -0
- package/.mise/locks/aws-cdk/2.1146.1/package.json +7 -0
- package/.mise/locks/azure-cli/2.91.0~a837e014/pyproject.toml +5 -0
- package/.mise/locks/azure-cli/2.91.0~a837e014/uv.lock +2794 -0
- package/.mise/locks/gemini/0.66.0~84aac8e6/aube-lock.yaml +123 -0
- package/.mise/locks/gemini/0.66.0~84aac8e6/package.json +12 -0
- package/.mise/locks/npm-devcontainers-cli/0.90.0/aube-lock.yaml +24 -0
- package/.mise/locks/npm-devcontainers-cli/0.90.0/package.json +7 -0
- package/.mise/locks/npm-modelcontextprotocol-inspector/2.13.0~4c109bea/aube-lock.yaml +1432 -0
- package/.mise/locks/npm-modelcontextprotocol-inspector/2.13.0~4c109bea/package.json +7 -0
- package/.mise/locks/npm-vercel/65.8.0/aube-lock.yaml +3595 -0
- package/.mise/locks/npm-vercel/65.8.0/package.json +7 -0
- package/.mise/locks/npm-wrangler/4.156.0/aube-lock.yaml +895 -0
- package/.mise/locks/npm-wrangler/4.156.0/package.json +7 -0
- package/.mise/locks/npm-yarnpkg-cli-dist/4.18.1/aube-lock.yaml +24 -0
- package/.mise/locks/npm-yarnpkg-cli-dist/4.18.1/package.json +7 -0
- package/LICENSE +21 -0
- package/README.md +357 -2
- package/agents/antigravity-proxy.ts +3 -0
- package/agents/audit.md +89 -0
- package/agents/claude-proxy +5 -0
- package/agents/claude-proxy.ts +3 -0
- package/agents/codex-proxy.ts +3 -0
- package/agents/grok-proxy.ts +3 -0
- package/agents/managed-proxy.ts +55 -0
- package/agents/opencode-proxy.ts +3 -0
- package/agents/registry.json +209 -0
- package/agents/registry.schema.json +149 -0
- package/agents/setup.md +158 -0
- package/agents/t3.json +40 -0
- package/ansible/macos/manager.yml +30 -0
- package/ansible/macos/policy.json +9 -0
- package/ansible/monitoring/README.md +215 -0
- package/ansible/monitoring/agent-darwin.yml +198 -0
- package/ansible/monitoring/agent.yml +172 -0
- package/ansible/monitoring/beszel-agent.service +42 -0
- package/ansible/monitoring/compose.yml +74 -0
- package/ansible/monitoring/hub-darwin.yml +102 -0
- package/ansible/monitoring/hub-route-darwin.yml +51 -0
- package/ansible/monitoring/lock.json +16 -0
- package/ansible/net/remote-probe.sh +13 -0
- package/ansible/pyproject.toml +12 -0
- package/ansible/ubuntu/authorize-sudo.sh +51 -0
- package/ansible/ubuntu/baseline.yml +89 -0
- package/ansible/ubuntu/disk-gc.py +336 -0
- package/ansible/ubuntu/disk-guard.py +104 -0
- package/ansible/ubuntu/policy.json +52 -0
- package/ansible/ubuntu/recovery.yml +15 -0
- package/ansible/ubuntu/t3/policy.json +7 -0
- package/ansible/ubuntu/t3-desktop.yml +220 -0
- package/ansible/ubuntu/t3-health.sh +33 -0
- package/ansible/ubuntu/t3-policy.yml +76 -0
- package/ansible/ubuntu/t3-route.yml +28 -0
- package/ansible/ubuntu/tailscale.yml +78 -0
- package/ansible/ubuntu/tasks/credentials.yml +47 -0
- package/ansible/ubuntu/tasks/docker.yml +31 -0
- package/ansible/ubuntu/tasks/packages.yml +56 -0
- package/ansible/ubuntu/tasks/power.yml +51 -0
- package/ansible/ubuntu/tasks/recovery.yml +39 -0
- package/ansible/ubuntu/tasks/resources.yml +312 -0
- package/ansible/ubuntu/tasks/services.yml +31 -0
- package/ansible/ubuntu/tasks/ssh.yml +71 -0
- package/ansible/ubuntu/tasks/user.yml +120 -0
- package/ansible/uv.lock +190 -0
- package/bin/devfleet.mjs +36 -0
- package/bootstrap/bootstrap.ps1 +50 -0
- package/bootstrap/bootstrap.sh +80 -0
- package/bootstrap/devfleet +7 -0
- package/bootstrap/docker-desktop.sh +33 -0
- package/bootstrap/doctor.ts +234 -0
- package/bootstrap/fleet-manager-run.sh +103 -0
- package/bootstrap/fleet-remote-bootstrap.sh +258 -0
- package/bootstrap/global-config.ts +171 -0
- package/bootstrap/install-launcher.sh +10 -0
- package/bootstrap/native-aws.ts +86 -0
- package/bootstrap/native-t3.ts +154 -0
- package/bootstrap/npm.ts +358 -0
- package/bootstrap/release-version.ts +157 -0
- package/bootstrap/setup-status.ts +51 -0
- package/bootstrap/setup.ts +88 -0
- package/bootstrap/shell-path.ts +105 -0
- package/bootstrap/verify-t3-archive.py +49 -0
- package/bootstrap/worker-auth.ts +65 -0
- package/bootstrap/worker-github.ts +47 -0
- package/bootstrap/worker-tools.ts +24 -0
- package/cliproxy/AGENTS.md +11 -0
- package/cliproxy/README.md +269 -0
- package/cliproxy/clients/bootstrap.py +143 -0
- package/cliproxy/clients/launcher.py +57 -0
- package/cliproxy/compose.yaml +90 -0
- package/cliproxy/config.template.yaml +70 -0
- package/cliproxy/docs/cloud-adoption.md +16 -0
- package/cliproxy/docs/deployment.png +0 -0
- package/cliproxy/docs/deployment.svg +109 -0
- package/cliproxy/docs/health-workflow.example.yml +185 -0
- package/cliproxy/docs/migration.md +131 -0
- package/cliproxy/docs/model-health-report.schema.json +364 -0
- package/cliproxy/docs/model-health.md +393 -0
- package/cliproxy/image.lock.json +5 -0
- package/cliproxy/launchd/dev.devfleet.cliproxy-backup-check.plist +38 -0
- package/cliproxy/launchd/dev.devfleet.cliproxy-backup.plist +48 -0
- package/cliproxy/launchd/dev.devfleet.cliproxy-fleet.plist +32 -0
- package/cliproxy/launchd/dev.devfleet.cliproxy-gc.plist +38 -0
- package/cliproxy/launchd/dev.devfleet.cliproxy-monitor.plist +29 -0
- package/cliproxy/launchd/dev.devfleet.cliproxy-vault-renew.plist +31 -0
- package/cliproxy/models.json +1618 -0
- package/cliproxy/requirements.lock +78 -0
- package/cliproxy/scripts/accounts.py +38 -0
- package/cliproxy/scripts/audit.py +123 -0
- package/cliproxy/scripts/backup +234 -0
- package/cliproxy/scripts/check.py +34 -0
- package/cliproxy/scripts/checkpoint.py +226 -0
- package/cliproxy/scripts/cli.py +64 -0
- package/cliproxy/scripts/cliproxy_host.py +257 -0
- package/cliproxy/scripts/cloud_state.py +150 -0
- package/cliproxy/scripts/container-health +6 -0
- package/cliproxy/scripts/docker +24 -0
- package/cliproxy/scripts/external-health.py +79 -0
- package/cliproxy/scripts/fleet_config.py +43 -0
- package/cliproxy/scripts/fleet_sync.py +274 -0
- package/cliproxy/scripts/initialize +84 -0
- package/cliproxy/scripts/install-dependencies +48 -0
- package/cliproxy/scripts/install.py +406 -0
- package/cliproxy/scripts/model_health.py +16 -0
- package/cliproxy/scripts/modelhealth/__init__.py +5 -0
- package/cliproxy/scripts/modelhealth/checks.py +429 -0
- package/cliproxy/scripts/modelhealth/cli.py +117 -0
- package/cliproxy/scripts/modelhealth/config.py +186 -0
- package/cliproxy/scripts/modelhealth/emulator.py +536 -0
- package/cliproxy/scripts/modelhealth/expect.py +88 -0
- package/cliproxy/scripts/modelhealth/findings.py +183 -0
- package/cliproxy/scripts/modelhealth/protocol.py +330 -0
- package/cliproxy/scripts/modelhealth/redact.py +28 -0
- package/cliproxy/scripts/modelhealth/report.py +150 -0
- package/cliproxy/scripts/modelhealth/runner.py +390 -0
- package/cliproxy/scripts/modelhealth/scenario.py +131 -0
- package/cliproxy/scripts/modelhealth/schema.py +63 -0
- package/cliproxy/scripts/modelhealth/simulate.py +111 -0
- package/cliproxy/scripts/modelhealth/transport.py +213 -0
- package/cliproxy/scripts/monitor +68 -0
- package/cliproxy/scripts/ops +527 -0
- package/cliproxy/scripts/public-endpoint +127 -0
- package/cliproxy/scripts/recover_node.py +177 -0
- package/cliproxy/scripts/restore-verify +60 -0
- package/cliproxy/scripts/retention.py +144 -0
- package/cliproxy/scripts/service-start +7 -0
- package/cliproxy/scripts/validate-config +56 -0
- package/cliproxy/scripts/vault-access.py +57 -0
- package/cliproxy/scripts/vault-backup-login +55 -0
- package/cliproxy/scripts/vault-recovery +56 -0
- package/cliproxy/scripts/vault-renew +30 -0
- package/cliproxy/systemd/cliproxy-backup-check.service +10 -0
- package/cliproxy/systemd/cliproxy-backup-check.timer +8 -0
- package/cliproxy/systemd/cliproxy-backup.service +13 -0
- package/cliproxy/systemd/cliproxy-backup.timer +8 -0
- package/cliproxy/systemd/cliproxy-fleet.service +17 -0
- package/cliproxy/systemd/cliproxy-gc.service +13 -0
- package/cliproxy/systemd/cliproxy-gc.timer +8 -0
- package/cliproxy/systemd/cliproxy-monitor.service +8 -0
- package/cliproxy/systemd/cliproxy-monitor.timer +7 -0
- package/cliproxy/systemd/cliproxy-vault-renew.service +8 -0
- package/cliproxy/systemd/cliproxy-vault-renew.timer +7 -0
- package/cliproxy/vendor/LICENSE +22 -0
- package/cliproxy/vendor/config.upstream.yaml +1349 -0
- package/docs/consumer-setup.md +408 -0
- package/docs/declared.md +89 -0
- package/docs/machine-setup.md +399 -0
- package/docs/operations.md +276 -0
- package/docs/releasing.md +103 -0
- package/docs/state.md +254 -0
- package/docs/topology.md +67 -0
- package/docs/ubuntu-sync.md +351 -0
- package/docs/worker-contract.md +67 -0
- package/examples/fleet.json +100 -0
- package/examples/roster.json +13 -0
- package/llms.txt +35 -0
- package/mise.lock +1521 -0
- package/mise.toml +110 -0
- package/package.json +74 -4
- package/schema/fleet.schema.json +420 -0
- package/schema/roster.schema.json +77 -0
- package/src/agents/account-relay.ts +111 -0
- package/src/agents/accounts.ts +85 -0
- package/src/agents/audit.ts +77 -0
- package/src/agents/cli.ts +13 -0
- package/src/agents/command.ts +204 -0
- package/src/agents/converge.ts +123 -0
- package/src/agents/doctor.ts +107 -0
- package/src/agents/errors.ts +2 -0
- package/src/agents/files.ts +73 -0
- package/src/agents/fleet-verify.ts +165 -0
- package/src/agents/launch.ts +290 -0
- package/src/agents/native-verify.ts +371 -0
- package/src/agents/provider-errors.ts +57 -0
- package/src/agents/registry.ts +61 -0
- package/src/agents/render.ts +110 -0
- package/src/agents/round-trip.ts +192 -0
- package/src/agents/secrets.ts +13 -0
- package/src/agents/toolchain.ts +34 -0
- package/src/agents/verify-cache.ts +103 -0
- package/src/agents/verify.ts +127 -0
- package/src/auth-checks.ts +163 -0
- package/src/cli/agent-guide.ts +129 -0
- package/src/cli/app.ts +122 -0
- package/src/cli/cli.ts +28 -0
- package/src/cli/command-index.ts +57 -0
- package/src/cli/contracts.ts +25 -0
- package/src/cli/fleet-reference.ts +17 -0
- package/src/cli/fleet-state.ts +499 -0
- package/src/cli/fleet-sync-checkout.ts +130 -0
- package/src/cli/fleet-sync-enrollment.ts +140 -0
- package/src/cli/fleet-sync-forward.ts +134 -0
- package/src/cli/fleet-sync-manager.ts +249 -0
- package/src/cli/fleet-sync-monitoring.ts +185 -0
- package/src/cli/fleet-sync-output.ts +317 -0
- package/src/cli/fleet-sync-privilege.ts +27 -0
- package/src/cli/fleet-sync-progress.ts +109 -0
- package/src/cli/fleet-sync-remote.ts +242 -0
- package/src/cli/fleet-sync-report.ts +26 -0
- package/src/cli/fleet-sync-schedule.ts +138 -0
- package/src/cli/fleet-sync-state.ts +86 -0
- package/src/cli/fleet-sync-vault.ts +166 -0
- package/src/cli/fleet-sync.ts +375 -0
- package/src/cli/fleet.ts +165 -0
- package/src/cli/managed-health.ts +219 -0
- package/src/cli/node-deployment.ts +34 -0
- package/src/cli/node-sync.ts +121 -0
- package/src/cli/node-t3.ts +31 -0
- package/src/cli/options.ts +141 -0
- package/src/cli/probe-source.ts +7 -0
- package/src/cli/program.ts +185 -0
- package/src/cli/project.ts +51 -0
- package/src/cli/register-fleet.ts +296 -0
- package/src/cli/registry.ts +185 -0
- package/src/cli/repair-apply.ts +67 -0
- package/src/cli/repair-guard.ts +11 -0
- package/src/cli/repair-inspect.ts +150 -0
- package/src/cli/repair-script.ts +76 -0
- package/src/cli/repair.ts +209 -0
- package/src/cli/roster.ts +105 -0
- package/src/cli/search.ts +35 -0
- package/src/cli/settings.ts +67 -0
- package/src/cli/source-operations.ts +102 -0
- package/src/cli/t3/command.ts +90 -0
- package/src/cli/t3/register.ts +55 -0
- package/src/cli/tailnet.ts +127 -0
- package/src/cli/ui-actions.ts +133 -0
- package/src/cli/ui-controller.ts +113 -0
- package/src/cli/ui-data.ts +106 -0
- package/src/cli/ui-execute.ts +163 -0
- package/src/cli/ui-repairs.ts +34 -0
- package/src/cli/ui-reports.ts +211 -0
- package/src/cli/ui-targets.ts +100 -0
- package/src/cli/ui-terminal.ts +109 -0
- package/src/cli/ui-types.ts +49 -0
- package/src/cli/ui-wizard.ts +101 -0
- package/src/cli/ui.ts +9 -0
- package/src/cli/worker-health-output.ts +37 -0
- package/src/config.ts +186 -0
- package/src/configure.ts +255 -0
- package/src/definition.ts +290 -0
- package/src/diagnostics.ts +92 -0
- package/src/doctor-check.ts +10 -0
- package/src/errors.ts +1 -0
- package/src/fingerprint.ts +120 -0
- package/src/fleet-config.ts +174 -0
- package/src/input-hashes.ts +73 -0
- package/src/machine/local-source.ts +52 -0
- package/src/machine/lock-error.ts +2 -0
- package/src/machine/platform.ts +44 -0
- package/src/machine/source.ts +68 -0
- package/src/machine/state.ts +173 -0
- package/src/machine-health.ts +95 -0
- package/src/managed-source.ts +47 -0
- package/src/manager-failure.ts +48 -0
- package/src/manager-sync.ts +405 -0
- package/src/manager.ts +62 -0
- package/src/monitoring/agent-evidence.ts +146 -0
- package/src/monitoring/apply.ts +183 -0
- package/src/monitoring/beszel.ts +198 -0
- package/src/monitoring/enroll.ts +111 -0
- package/src/monitoring/hub-access.ts +114 -0
- package/src/monitoring/hub-backup.ts +121 -0
- package/src/monitoring/hub-viewer.ts +142 -0
- package/src/monitoring/hub.ts +93 -0
- package/src/monitoring/manager-agent.ts +73 -0
- package/src/monitoring/manager-hub.ts +248 -0
- package/src/monitoring/remote.ts +65 -0
- package/src/network-definition.ts +83 -0
- package/src/node-converge-linux.ts +374 -0
- package/src/node-converge-shared.ts +83 -0
- package/src/node-converge.ts +40 -0
- package/src/onboard.ts +35 -0
- package/src/package-info.ts +45 -0
- package/src/platform/ansible-receipt.ts +128 -0
- package/src/platform/ansible.ts +66 -0
- package/src/platform/darwin.ts +203 -0
- package/src/platform/detect.ts +11 -0
- package/src/platform/fallback.ts +110 -0
- package/src/platform/index.ts +38 -0
- package/src/platform/launchd.ts +98 -0
- package/src/platform/linux.ts +139 -0
- package/src/platform/ps.ts +45 -0
- package/src/platform/resource-verify.ts +132 -0
- package/src/platform/systemd-user.ts +95 -0
- package/src/platform/t3-policy.ts +137 -0
- package/src/platform/t3-route.ts +68 -0
- package/src/platform/t3-service.ts +75 -0
- package/src/platform/types.ts +123 -0
- package/src/platform/ubuntu-audit.ts +66 -0
- package/src/platform/ubuntu-verify.ts +105 -0
- package/src/platform/windows.ts +148 -0
- package/src/power-recovery.ts +55 -0
- package/src/probe.ts +51 -0
- package/src/protocol.ts +119 -0
- package/src/proxy-converge.ts +85 -0
- package/src/remote-bootstrap.ts +210 -0
- package/src/remote-entry.ts +89 -0
- package/src/remote-lock.ts +12 -0
- package/src/repair-catalog.ts +206 -0
- package/src/repair-plan.ts +78 -0
- package/src/repair-runner.ts +84 -0
- package/src/report.ts +71 -0
- package/src/runner.ts +104 -0
- package/src/secrets.ts +169 -0
- package/src/service-auth.ts +115 -0
- package/src/ssh.ts +135 -0
- package/src/state/bucket.ts +208 -0
- package/src/state/check.ts +74 -0
- package/src/state/connection.ts +92 -0
- package/src/state/enrollment.ts +45 -0
- package/src/state/legacy.ts +89 -0
- package/src/state/litestream.ts +143 -0
- package/src/state/operator.ts +20 -0
- package/src/state/paths.ts +40 -0
- package/src/state/roster.ts +62 -0
- package/src/state/store.ts +169 -0
- package/src/state/sync.ts +352 -0
- package/src/sync.ts +104 -0
- package/src/t3/accounts.ts +81 -0
- package/src/t3/desired.ts +165 -0
- package/src/t3/files.ts +81 -0
- package/src/t3/names-check.ts +53 -0
- package/src/t3/names.ts +155 -0
- package/src/t3/plan.ts +82 -0
- package/src/t3/providers.ts +74 -0
- package/src/t3/reconcile.ts +123 -0
- package/src/t3-endpoint.ts +104 -0
- package/src/t3-linked-check.ts +123 -0
- package/src/tailnet-health.ts +231 -0
- package/src/tailnet-matching.ts +37 -0
- package/src/tailscale.ts +141 -0
- package/src/toolchain-checks.ts +108 -0
- package/src/topology.ts +24 -0
- package/src/transport.ts +122 -0
- package/src/types.ts +99 -0
- package/src/vault-config.ts +38 -0
- package/src/worker-access-checks.ts +168 -0
- package/src/worker-capacity.ts +34 -0
- package/src/worker-host-checks.ts +181 -0
- package/src/worker-policy.ts +64 -0
- package/src/worker-readiness.ts +64 -0
- package/src/workstation-checks.ts +29 -0
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
import { diagnostic, type Diagnostic } from './diagnostics';
|
|
2
|
+
import {
|
|
3
|
+
blocking,
|
|
4
|
+
type Check,
|
|
5
|
+
type FleetNode,
|
|
6
|
+
type NodeReport,
|
|
7
|
+
type Runner,
|
|
8
|
+
} from './types';
|
|
9
|
+
import { discoverTailnet, type TailnetComputer } from './tailscale';
|
|
10
|
+
import { matchComputers } from './tailnet-matching';
|
|
11
|
+
import { sshArgs, sshFailure } from './ssh';
|
|
12
|
+
import type { FleetDefinition, MachineDefinition } from './definition';
|
|
13
|
+
import { publicHubUrl } from './monitoring/beszel';
|
|
14
|
+
import {
|
|
15
|
+
convergeable,
|
|
16
|
+
evaluatePlatform,
|
|
17
|
+
PLATFORM_PROBE,
|
|
18
|
+
type MachinePlatform,
|
|
19
|
+
} from './machine/platform';
|
|
20
|
+
|
|
21
|
+
export interface ManagerNode extends NodeReport {
|
|
22
|
+
/** The singleton manager converges locally; workers over SSH. */
|
|
23
|
+
role?: 'manager' | 'worker';
|
|
24
|
+
reachability: 'online' | 'offline' | 'missing' | 'conflict' | 'unknown';
|
|
25
|
+
desiredRevision: string | null;
|
|
26
|
+
appliedRevision: string | null;
|
|
27
|
+
durationMs: number;
|
|
28
|
+
diagnostics?: Diagnostic[];
|
|
29
|
+
}
|
|
30
|
+
export interface ManagerReport {
|
|
31
|
+
version: 3;
|
|
32
|
+
command: 'fleet-sync';
|
|
33
|
+
execution: 'manager-push';
|
|
34
|
+
ok: boolean;
|
|
35
|
+
applied: boolean;
|
|
36
|
+
desiredRevision: string | null;
|
|
37
|
+
checks: Check[];
|
|
38
|
+
nodes: ManagerNode[];
|
|
39
|
+
candidates: number;
|
|
40
|
+
dashboardUrl: string | null;
|
|
41
|
+
}
|
|
42
|
+
export type DeployNode = (
|
|
43
|
+
node: FleetNode,
|
|
44
|
+
revision: string
|
|
45
|
+
) => Promise<NodeReport>;
|
|
46
|
+
|
|
47
|
+
/** Minimal read-only POSIX inspection requires no workspace, Node, Bun or Git. */
|
|
48
|
+
export async function inspectBootstrap(
|
|
49
|
+
node: FleetNode,
|
|
50
|
+
run: Runner,
|
|
51
|
+
os: MachinePlatform = 'linux'
|
|
52
|
+
): Promise<Check[]> {
|
|
53
|
+
const result = await run([...sshArgs(node), PLATFORM_PROBE], {
|
|
54
|
+
timeoutMs: 20_000,
|
|
55
|
+
});
|
|
56
|
+
if (!result.ok) return [sshFailure(result)];
|
|
57
|
+
const baseline = evaluatePlatform(os, result.stdout);
|
|
58
|
+
return [
|
|
59
|
+
{
|
|
60
|
+
id: 'ssh:fresh-session',
|
|
61
|
+
status: 'pass',
|
|
62
|
+
summary:
|
|
63
|
+
'Fresh host-key-verified SSH session succeeded without remote developer prerequisites.',
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: baseline.id,
|
|
67
|
+
status: baseline.supported ? 'pass' : 'fail',
|
|
68
|
+
summary: baseline.summary,
|
|
69
|
+
remedy: {
|
|
70
|
+
action:
|
|
71
|
+
'Review the release/architecture migration in source. No operating-system upgrade or downgrade is performed.',
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** One invocation, one immutable revision. Serial application bounds pressure and
|
|
78
|
+
* lets a failed declared canary stop promotion; diagnostics still cover all nodes.
|
|
79
|
+
*/
|
|
80
|
+
export async function synchronizeManager(
|
|
81
|
+
input: {
|
|
82
|
+
definition: FleetDefinition;
|
|
83
|
+
nodes: FleetNode[];
|
|
84
|
+
revision: string;
|
|
85
|
+
apply: boolean;
|
|
86
|
+
canary?: string;
|
|
87
|
+
progress?: (node: string, phase: string) => void;
|
|
88
|
+
/** Called once per node as soon as its result is final. */
|
|
89
|
+
finished?: (node: ManagerNode) => void;
|
|
90
|
+
/** Converges the manager locally; omitted in tests. */
|
|
91
|
+
manager?: () => Promise<ManagerNode>;
|
|
92
|
+
},
|
|
93
|
+
run: Runner,
|
|
94
|
+
deploy: DeployNode
|
|
95
|
+
): Promise<ManagerReport> {
|
|
96
|
+
const tailnet = await discoverTailnet(run);
|
|
97
|
+
const matches = await matchComputers(input.nodes, tailnet.computers, run);
|
|
98
|
+
const report: ManagerReport = {
|
|
99
|
+
version: 3,
|
|
100
|
+
command: 'fleet-sync',
|
|
101
|
+
execution: 'manager-push',
|
|
102
|
+
ok: false,
|
|
103
|
+
applied: false,
|
|
104
|
+
desiredRevision: input.revision,
|
|
105
|
+
checks: tailnet.checks,
|
|
106
|
+
// Printed for people: the stable public name, never a tailnet address.
|
|
107
|
+
dashboardUrl: publicHubUrl(),
|
|
108
|
+
nodes: [],
|
|
109
|
+
candidates: tailnet.computers.filter(
|
|
110
|
+
(peer) => !peer.self && !matches.get(peer.id)?.length
|
|
111
|
+
).length,
|
|
112
|
+
};
|
|
113
|
+
const managerRun = input.manager ? convergeManagerNode(input) : null;
|
|
114
|
+
// The manager finishes first: updating it may restart the proxy every
|
|
115
|
+
// worker is then verified against.
|
|
116
|
+
if (managerRun) await managerRun;
|
|
117
|
+
const workers = await Promise.all(
|
|
118
|
+
input.definition.workers.filter(convergeable).map((machine) => {
|
|
119
|
+
input.progress?.(
|
|
120
|
+
machine.id,
|
|
121
|
+
'Inspecting enrolled identity and fresh SSH access'
|
|
122
|
+
);
|
|
123
|
+
return inspectManagerNode(
|
|
124
|
+
machine,
|
|
125
|
+
{
|
|
126
|
+
nodes: input.nodes,
|
|
127
|
+
revision: input.revision,
|
|
128
|
+
computers: tailnet.computers,
|
|
129
|
+
matches,
|
|
130
|
+
},
|
|
131
|
+
run
|
|
132
|
+
);
|
|
133
|
+
})
|
|
134
|
+
);
|
|
135
|
+
report.nodes.push(...workers);
|
|
136
|
+
if (input.apply) await applyNodes(report, input, deploy);
|
|
137
|
+
else workers.forEach((node) => input.finished?.(node));
|
|
138
|
+
if (managerRun) report.nodes.unshift(await managerRun);
|
|
139
|
+
report.ok = ready(report);
|
|
140
|
+
for (const node of report.nodes)
|
|
141
|
+
node.diagnostics = node.checks.map((check) => diagnostic(node.node, check));
|
|
142
|
+
return report;
|
|
143
|
+
}
|
|
144
|
+
async function convergeManagerNode(input: {
|
|
145
|
+
definition: FleetDefinition;
|
|
146
|
+
progress?: (node: string, phase: string) => void;
|
|
147
|
+
finished?: (node: ManagerNode) => void;
|
|
148
|
+
manager?: () => Promise<ManagerNode>;
|
|
149
|
+
}): Promise<ManagerNode> {
|
|
150
|
+
input.progress?.(
|
|
151
|
+
input.definition.manager.id,
|
|
152
|
+
'Converging the manager and its singleton services locally'
|
|
153
|
+
);
|
|
154
|
+
const manager = await input.manager!();
|
|
155
|
+
input.finished?.(manager);
|
|
156
|
+
return manager;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Ready when nothing required failed; warnings and skips do not count. */
|
|
160
|
+
function ready(report: ManagerReport): boolean {
|
|
161
|
+
return (
|
|
162
|
+
!report.checks.some(blocking) &&
|
|
163
|
+
report.nodes.length > 0 &&
|
|
164
|
+
report.nodes.every(
|
|
165
|
+
(node) => node.checks.length > 0 && !node.checks.some(blocking)
|
|
166
|
+
)
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function applyNodes(
|
|
171
|
+
report: ManagerReport,
|
|
172
|
+
input: {
|
|
173
|
+
nodes: FleetNode[];
|
|
174
|
+
revision: string;
|
|
175
|
+
canary?: string;
|
|
176
|
+
progress?: (node: string, phase: string) => void;
|
|
177
|
+
finished?: (node: ManagerNode) => void;
|
|
178
|
+
},
|
|
179
|
+
deploy: DeployNode
|
|
180
|
+
): Promise<void> {
|
|
181
|
+
const workers = report.nodes.filter((node) => node.role !== 'manager');
|
|
182
|
+
// A revision new to the fleet is proven on one worker first. When the run
|
|
183
|
+
// selects only later workers, the first selected one proves it instead:
|
|
184
|
+
// narrowing a run never skips the canary.
|
|
185
|
+
const canary = input.canary
|
|
186
|
+
? (workers.find((node) => node.node === input.canary) ?? workers[0])
|
|
187
|
+
: undefined;
|
|
188
|
+
let blocked = report.checks.some(blocking);
|
|
189
|
+
const step = async (result: ManagerNode) => {
|
|
190
|
+
const deployable = mayDeploy(result, input.nodes);
|
|
191
|
+
if (await rollOut(result, { deployable, blocked }, input, deploy))
|
|
192
|
+
report.applied = true;
|
|
193
|
+
input.finished?.(result);
|
|
194
|
+
return deployable;
|
|
195
|
+
};
|
|
196
|
+
// The canary goes first; a failed canary stops promotion. The remaining
|
|
197
|
+
// workers are independent, so they converge concurrently.
|
|
198
|
+
if (canary) {
|
|
199
|
+
const deployable = await step(canary);
|
|
200
|
+
if (!deployable || canary.checks.some(blocksPromotion)) blocked = true;
|
|
201
|
+
}
|
|
202
|
+
await Promise.all(workers.filter((node) => node !== canary).map(step));
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** One node's rollout step; true when its remote operation ran. */
|
|
206
|
+
async function rollOut(
|
|
207
|
+
result: ManagerNode,
|
|
208
|
+
{ deployable, blocked }: { deployable: boolean; blocked: boolean },
|
|
209
|
+
input: {
|
|
210
|
+
nodes: FleetNode[];
|
|
211
|
+
revision: string;
|
|
212
|
+
progress?: (node: string, phase: string) => void;
|
|
213
|
+
},
|
|
214
|
+
deploy: DeployNode
|
|
215
|
+
): Promise<boolean> {
|
|
216
|
+
if (!deployable) return false;
|
|
217
|
+
if (blocked) {
|
|
218
|
+
result.checks.push({
|
|
219
|
+
id: 'rollout:promotion',
|
|
220
|
+
status: 'fail',
|
|
221
|
+
summary: 'FLEET_PROMOTION_BLOCKED',
|
|
222
|
+
remedy: {
|
|
223
|
+
action:
|
|
224
|
+
'Resolve the discovery or canary failure above, then rerun the same approved revision.',
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
input.progress?.(
|
|
230
|
+
result.node,
|
|
231
|
+
'Applying the approved revision and verifying postconditions'
|
|
232
|
+
);
|
|
233
|
+
return deployNode(result, input, deploy);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Deploys one node and records its evidence; true when the remote ran. */
|
|
237
|
+
async function deployNode(
|
|
238
|
+
result: ManagerNode,
|
|
239
|
+
input: { nodes: FleetNode[]; revision: string },
|
|
240
|
+
deploy: DeployNode
|
|
241
|
+
): Promise<boolean> {
|
|
242
|
+
const started = Date.now();
|
|
243
|
+
let ran = false;
|
|
244
|
+
try {
|
|
245
|
+
const outcome = await deploy(
|
|
246
|
+
input.nodes.find((node) => node.id === result.node)!,
|
|
247
|
+
input.revision
|
|
248
|
+
);
|
|
249
|
+
ran = true;
|
|
250
|
+
result.checks.push(...outcome.checks);
|
|
251
|
+
if (!outcome.checks.length)
|
|
252
|
+
result.checks.push({
|
|
253
|
+
id: 'rollout:evidence',
|
|
254
|
+
status: 'fail',
|
|
255
|
+
summary: 'FLEET_VERIFICATION_MISSING',
|
|
256
|
+
});
|
|
257
|
+
} catch {
|
|
258
|
+
result.checks.push({
|
|
259
|
+
id: 'rollout:operation',
|
|
260
|
+
status: 'fail',
|
|
261
|
+
summary: 'FLEET_REMOTE_OPERATION_FAILED',
|
|
262
|
+
remedy: {
|
|
263
|
+
action:
|
|
264
|
+
'Inspect the remote operation lock before retrying. A disconnected manager does not establish that installation stopped.',
|
|
265
|
+
},
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
result.durationMs += Date.now() - started;
|
|
269
|
+
if (!result.checks.some(blocking)) result.appliedRevision = input.revision;
|
|
270
|
+
return ran;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function connectionState(
|
|
274
|
+
conflict: boolean,
|
|
275
|
+
peer: TailnetComputer | undefined
|
|
276
|
+
): ManagerNode['reachability'] {
|
|
277
|
+
if (conflict) return 'conflict';
|
|
278
|
+
if (!peer) return 'missing';
|
|
279
|
+
return peer.online ? 'online' : 'offline';
|
|
280
|
+
}
|
|
281
|
+
async function inspectManagerNode(
|
|
282
|
+
machine: MachineDefinition,
|
|
283
|
+
context: {
|
|
284
|
+
nodes: FleetNode[];
|
|
285
|
+
revision: string;
|
|
286
|
+
computers: TailnetComputer[];
|
|
287
|
+
matches: Map<string, FleetNode[]>;
|
|
288
|
+
},
|
|
289
|
+
run: Runner
|
|
290
|
+
): Promise<ManagerNode> {
|
|
291
|
+
const { nodes, revision, computers, matches } = context;
|
|
292
|
+
const id = machine.id;
|
|
293
|
+
const started = Date.now();
|
|
294
|
+
const node = nodes.find((entry) => entry.id === id);
|
|
295
|
+
const peers = computers.filter((peer) =>
|
|
296
|
+
matches.get(peer.id)?.some((entry) => entry.id === id)
|
|
297
|
+
);
|
|
298
|
+
const peer = peers[0];
|
|
299
|
+
const conflict =
|
|
300
|
+
peers.length > 1 || (peer && matches.get(peer.id)!.length > 1);
|
|
301
|
+
const checks: Check[] = [];
|
|
302
|
+
if (!node)
|
|
303
|
+
checks.push({
|
|
304
|
+
id: 'enrollment:connection',
|
|
305
|
+
status: 'fail',
|
|
306
|
+
summary: 'FLEET_CONNECTION_REQUIRED',
|
|
307
|
+
remedy: {
|
|
308
|
+
action:
|
|
309
|
+
'Add this canonical node’s reviewed SSH alias and managed workspace path to the private manager overrides. Do not substitute a discovered peer.',
|
|
310
|
+
},
|
|
311
|
+
});
|
|
312
|
+
else if (conflict)
|
|
313
|
+
checks.push({
|
|
314
|
+
id: 'tailscale:identity',
|
|
315
|
+
status: 'fail',
|
|
316
|
+
summary: 'TAILSCALE_IDENTITY_CONFLICT',
|
|
317
|
+
remedy: {
|
|
318
|
+
action:
|
|
319
|
+
'Resolve duplicate alias/device mappings before enrollment or apply.',
|
|
320
|
+
},
|
|
321
|
+
});
|
|
322
|
+
else {
|
|
323
|
+
checks.push(...(await inspectBootstrap(node, run, machine.os)));
|
|
324
|
+
checks.push(...identityChecks(node, peer));
|
|
325
|
+
}
|
|
326
|
+
if (!peer || !peer.online)
|
|
327
|
+
checks.push({
|
|
328
|
+
id: 'tailscale:reachability',
|
|
329
|
+
status: 'fail',
|
|
330
|
+
summary: peer ? 'TAILSCALE_NODE_OFFLINE' : 'TAILSCALE_NODE_MISSING',
|
|
331
|
+
remedy: {
|
|
332
|
+
action:
|
|
333
|
+
'Restore the intended node’s private connection. A trusted LAN SSH route can be inspected, but discovery never enrolls a replacement.',
|
|
334
|
+
},
|
|
335
|
+
});
|
|
336
|
+
return {
|
|
337
|
+
node: id,
|
|
338
|
+
role: 'worker',
|
|
339
|
+
checks,
|
|
340
|
+
desiredRevision: revision,
|
|
341
|
+
appliedRevision: null,
|
|
342
|
+
durationMs: Date.now() - started,
|
|
343
|
+
reachability: connectionState(Boolean(conflict), peer),
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function identityChecks(
|
|
348
|
+
node: FleetNode,
|
|
349
|
+
peer: TailnetComputer | undefined
|
|
350
|
+
): Check[] {
|
|
351
|
+
const checks: Check[] = [];
|
|
352
|
+
if (!node.tailscaleId || (peer && peer.id !== node.tailscaleId))
|
|
353
|
+
checks.push({
|
|
354
|
+
id: 'tailscale:enrolled-identity',
|
|
355
|
+
status: 'fail',
|
|
356
|
+
summary: node.tailscaleId
|
|
357
|
+
? 'TAILSCALE_DEVICE_REPLACED'
|
|
358
|
+
: 'TAILSCALE_ENROLLMENT_REQUIRED',
|
|
359
|
+
remedy: {
|
|
360
|
+
action:
|
|
361
|
+
'Verify the device identity and SSH host key, then record its stable tailscaleId in the reviewed private connection override. Names alone cannot authorize replacement.',
|
|
362
|
+
},
|
|
363
|
+
});
|
|
364
|
+
return checks;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function mayDeploy(result: ManagerNode, nodes: FleetNode[]): boolean {
|
|
368
|
+
const allowBootstrap =
|
|
369
|
+
nodes.find((node) => node.id === result.node)?.bootstrapViaSsh === true;
|
|
370
|
+
return result.checks.every(
|
|
371
|
+
(check) =>
|
|
372
|
+
!blocking(check) ||
|
|
373
|
+
(allowBootstrap &&
|
|
374
|
+
[
|
|
375
|
+
'TAILSCALE_NODE_OFFLINE',
|
|
376
|
+
'TAILSCALE_NODE_MISSING',
|
|
377
|
+
'TAILSCALE_ENROLLMENT_REQUIRED',
|
|
378
|
+
].includes(check.summary))
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/** Service/account acceptance remains required for readiness, but a missing
|
|
383
|
+
* external account, dashboard or client pairing must not stop independent OS
|
|
384
|
+
* configuration on other nodes. Actual deployment failures stop promotion.
|
|
385
|
+
*/
|
|
386
|
+
export function blocksPromotion(check: Check): boolean {
|
|
387
|
+
if (!blocking(check)) return false;
|
|
388
|
+
if (check.id === 't3:service' && T3_MAINTENANCE.has(check.summary))
|
|
389
|
+
return false;
|
|
390
|
+
return !/^(worker:|beszel:|provider:|agent:|proxy:|t3:end-to-end$|t3:paired-access$|t3:policy-loaded$|ubuntu:boot-policy$)/.test(
|
|
391
|
+
check.id
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/** Native T3 outcomes that need an operator maintenance window (an active
|
|
396
|
+
* listener or running release must never be killed by routine sync). They are
|
|
397
|
+
* reported as failures but do not block independent OS policy on other nodes.
|
|
398
|
+
*/
|
|
399
|
+
const T3_MAINTENANCE = new Set([
|
|
400
|
+
'T3_LISTENER_CONFLICT',
|
|
401
|
+
'T3_ACTIVE_LISTENER_MIGRATION_REQUIRED',
|
|
402
|
+
'T3_MAINTENANCE_REQUIRED',
|
|
403
|
+
'T3_POLICY_RESTART_PENDING',
|
|
404
|
+
'T3_DESKTOP_MIGRATION_REQUIRED',
|
|
405
|
+
]);
|
package/src/manager.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { compareFingerprints, fingerprint } from './fingerprint';
|
|
2
|
+
import { probe } from './probe';
|
|
3
|
+
import { mapBounded } from './report';
|
|
4
|
+
import { bundleProbe, inspectRemote } from './transport';
|
|
5
|
+
import type { Capability, FleetNode, NodeReport, Runner } from './types';
|
|
6
|
+
|
|
7
|
+
export interface DoctorOptions {
|
|
8
|
+
root: string;
|
|
9
|
+
timeoutMs: number;
|
|
10
|
+
local: boolean;
|
|
11
|
+
required: Capability[];
|
|
12
|
+
source?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function doctor(
|
|
16
|
+
nodes: FleetNode[],
|
|
17
|
+
options: DoctorOptions,
|
|
18
|
+
run: Runner
|
|
19
|
+
): Promise<NodeReport[]> {
|
|
20
|
+
const local = await probe(options.root, options.required, run);
|
|
21
|
+
const agent = await run(['ssh-add', '-l']);
|
|
22
|
+
const reports: NodeReport[] = [
|
|
23
|
+
{
|
|
24
|
+
node: 'manager',
|
|
25
|
+
snapshot: local,
|
|
26
|
+
checks: [
|
|
27
|
+
...local.checks,
|
|
28
|
+
{
|
|
29
|
+
id: 'ssh:agent',
|
|
30
|
+
status: agent.ok ? 'pass' : 'warn',
|
|
31
|
+
summary: agent.ok
|
|
32
|
+
? 'SSH agent exposes at least one identity.'
|
|
33
|
+
: 'No accessible SSH agent identities; explicit IdentityFile keys may still work.',
|
|
34
|
+
...(!agent.ok
|
|
35
|
+
? {
|
|
36
|
+
remedy: {
|
|
37
|
+
action:
|
|
38
|
+
'Unlock/load the intended key in the manager session. Check SSH_AUTH_SOCK and GUI/editor agent visibility; do not copy private keys.',
|
|
39
|
+
},
|
|
40
|
+
}
|
|
41
|
+
: {}),
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
];
|
|
46
|
+
if (options.local || nodes.length === 0) return reports;
|
|
47
|
+
const source = options.source ?? (await bundleProbe());
|
|
48
|
+
const reference = await fingerprint(options.root, run);
|
|
49
|
+
const remote = await mapBounded(nodes, async (node) => {
|
|
50
|
+
const report = await inspectRemote(
|
|
51
|
+
node,
|
|
52
|
+
{ action: 'probe', source, timeoutMs: options.timeoutMs },
|
|
53
|
+
run
|
|
54
|
+
);
|
|
55
|
+
if (report.snapshot)
|
|
56
|
+
report.checks.push(
|
|
57
|
+
...compareFingerprints(reference, report.snapshot.fingerprint)
|
|
58
|
+
);
|
|
59
|
+
return report;
|
|
60
|
+
});
|
|
61
|
+
return [...reports, ...remote];
|
|
62
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import type { Check, Runner } from '../types';
|
|
2
|
+
|
|
3
|
+
/** Where installing the agent writes: the pinned build and its state. */
|
|
4
|
+
export const AGENT_PATHS = ['/usr/local/lib', '/var/lib'] as const;
|
|
5
|
+
/** Room for the archive, the extracted build and the agent's state. */
|
|
6
|
+
export const AGENT_FREE_KIB = 256 * 1024;
|
|
7
|
+
|
|
8
|
+
/** Measures, before anything is installed, that each path the agent writes
|
|
9
|
+
* to has room. A full disk is named as such instead of surfacing later as an
|
|
10
|
+
* incomplete installation.
|
|
11
|
+
*/
|
|
12
|
+
export async function agentDisk(run: Runner): Promise<Check> {
|
|
13
|
+
const result = await run(['df', '-Pk', ...AGENT_PATHS], {
|
|
14
|
+
timeoutMs: 10_000,
|
|
15
|
+
});
|
|
16
|
+
const free = result.ok ? available(result.stdout) : null;
|
|
17
|
+
if (!free || free.length !== AGENT_PATHS.length)
|
|
18
|
+
return {
|
|
19
|
+
id: 'beszel:disk',
|
|
20
|
+
status: 'fail',
|
|
21
|
+
summary: 'BESZEL_DISK_UNVERIFIED',
|
|
22
|
+
remedy: {
|
|
23
|
+
action: `Free space under ${AGENT_PATHS.join(' and ')} could not be measured. Check that df works for the fleet user, then rerun fleet-sync. Nothing was installed.`,
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
const least = Math.min(...free);
|
|
27
|
+
if (least < AGENT_FREE_KIB)
|
|
28
|
+
return {
|
|
29
|
+
id: 'beszel:disk',
|
|
30
|
+
status: 'fail',
|
|
31
|
+
summary: 'BESZEL_DISK_FULL',
|
|
32
|
+
remedy: {
|
|
33
|
+
action: `The monitoring agent needs ${AGENT_FREE_KIB / 1024} MiB free under each of ${AGENT_PATHS.join(' and ')}; the fullest has ${Math.floor(least / 1024)} MiB. Free space on the worker, then rerun fleet-sync. Nothing was installed and the running agent was left as it was.`,
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
return {
|
|
37
|
+
id: 'beszel:disk',
|
|
38
|
+
status: 'pass',
|
|
39
|
+
summary: `At least ${Math.floor(least / 1024)} MiB free where the agent is installed.`,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The "Available" column of POSIX df output, one value per path. */
|
|
44
|
+
function available(output: string): number[] | null {
|
|
45
|
+
const rows = output.trim().split('\n').slice(1);
|
|
46
|
+
const values = rows.map((row) => Number(row.trim().split(/\s+/)[3]));
|
|
47
|
+
return values.length && values.every(Number.isSafeInteger) ? values : null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** What the pinned agent (Beszel 0.21.1) logs about its connection to the
|
|
51
|
+
* hub, from its source:
|
|
52
|
+
* - `agent/connection_manager.go` logs "WebSocket connected" once the hub
|
|
53
|
+
* proved its key;
|
|
54
|
+
* - the hub answers an unknown token with 401 (`internal/hub/agent_connect.go`),
|
|
55
|
+
* which the WebSocket library reports as "unexpected status code: 401";
|
|
56
|
+
* - a hub whose key is not the one the agent was given fails
|
|
57
|
+
* `verifySignature`: "invalid signature - check KEY value";
|
|
58
|
+
* - a hub that is down behind its private route answers 502, 503 or 504.
|
|
59
|
+
* Only these fixed texts are matched; nothing of the log is reported.
|
|
60
|
+
*/
|
|
61
|
+
const OUTCOMES: { pattern: RegExp; check: Omit<Check, 'id'> }[] = [
|
|
62
|
+
{
|
|
63
|
+
pattern: /WebSocket connected/,
|
|
64
|
+
check: {
|
|
65
|
+
status: 'pass',
|
|
66
|
+
summary: 'The agent is connected to the hub, which proved its key.',
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
pattern: /unexpected status code: 401\b/,
|
|
71
|
+
check: {
|
|
72
|
+
status: 'fail',
|
|
73
|
+
summary: 'BESZEL_AGENT_TOKEN_REJECTED',
|
|
74
|
+
remedy: {
|
|
75
|
+
action:
|
|
76
|
+
"The hub refused this worker's agent token. Rerun fleet-sync from the manager so the worker's enrollment is resumed and its token delivered again. Never register a second record for the worker.",
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
pattern: /invalid signature - check KEY value/,
|
|
82
|
+
check: {
|
|
83
|
+
status: 'fail',
|
|
84
|
+
summary: 'BESZEL_AGENT_KEY_REJECTED',
|
|
85
|
+
remedy: {
|
|
86
|
+
action:
|
|
87
|
+
"The hub the agent reached did not prove the key it was given. Make sure the worker's agent points at the manager's hub, then rerun fleet-sync from the manager so the hub's key is delivered again.",
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
pattern: /unexpected status code: 50[234]\b/,
|
|
93
|
+
check: {
|
|
94
|
+
status: 'warn',
|
|
95
|
+
summary: 'BESZEL_HUB_UNAVAILABLE_TO_AGENT',
|
|
96
|
+
remedy: {
|
|
97
|
+
action:
|
|
98
|
+
'The private route answered but the hub behind it did not. Keep the manager awake with Docker Desktop running and rerun fleet-sync there; the agent reconnects by itself.',
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
/** The agent's newest connection outcome, read from its own log. The log
|
|
105
|
+
* holds host names, so only the outcome is reported.
|
|
106
|
+
*/
|
|
107
|
+
export async function agentConnection(run: Runner): Promise<Check> {
|
|
108
|
+
const result = await run(
|
|
109
|
+
[
|
|
110
|
+
'sudo',
|
|
111
|
+
'-n',
|
|
112
|
+
'journalctl',
|
|
113
|
+
'--unit=beszel-agent.service',
|
|
114
|
+
'--output=cat',
|
|
115
|
+
'--no-pager',
|
|
116
|
+
'--lines=200',
|
|
117
|
+
],
|
|
118
|
+
{ timeoutMs: 10_000 }
|
|
119
|
+
);
|
|
120
|
+
if (!result.ok)
|
|
121
|
+
return {
|
|
122
|
+
id: 'beszel:hub-connection',
|
|
123
|
+
status: 'skip',
|
|
124
|
+
summary: 'BESZEL_AGENT_LOG_UNAVAILABLE',
|
|
125
|
+
remedy: {
|
|
126
|
+
action:
|
|
127
|
+
"The agent's log could not be read, so how the hub answered it is unknown. The hub's own evidence in fleet-status --workers still applies.",
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
return { id: 'beszel:hub-connection', ...newestOutcome(result.stdout) };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function newestOutcome(log: string): Omit<Check, 'id'> {
|
|
134
|
+
const lines = log.split('\n').reverse();
|
|
135
|
+
for (const line of lines)
|
|
136
|
+
for (const outcome of OUTCOMES)
|
|
137
|
+
if (outcome.pattern.test(line)) return outcome.check;
|
|
138
|
+
return {
|
|
139
|
+
status: 'skip',
|
|
140
|
+
summary: 'BESZEL_AGENT_CONNECTION_PENDING',
|
|
141
|
+
remedy: {
|
|
142
|
+
action:
|
|
143
|
+
'The agent has not reached the hub yet. The next fleet-sync, or fleet-status --workers, reports whether it did.',
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}
|