@crvouga/dev-fleet 0.7.0 → 0.9.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/README.md +88 -7
- package/agents/audit.md +18 -18
- package/ansible/agents/agents.yml +134 -0
- package/ansible/agents/templates/antigravity.settings.json.j2 +3 -0
- package/ansible/agents/templates/claude.settings.json.j2 +3 -0
- package/ansible/agents/templates/codex.config.toml.j2 +11 -0
- package/ansible/agents/templates/gemini.settings.json.j2 +14 -0
- package/ansible/agents/templates/grok.config.toml.j2 +19 -0
- package/ansible/agents/templates/opencode.json.j2 +24 -0
- package/ansible/agents/templates/receipt.json.j2 +10 -0
- package/ansible/ansible.cfg +14 -0
- package/ansible/github/git.yml +82 -0
- package/ansible/macos/manager-privilege.yml +83 -0
- package/ansible/macos/manager.yml +164 -9
- package/ansible/macos/policy.json +13 -0
- package/ansible/macos/t3-policy.yml +5 -3
- package/ansible/macos/vault-renew.yml +85 -0
- package/ansible/macos/worker-host-key.yml +33 -0
- package/ansible/monitoring/agent-darwin.yml +3 -0
- package/ansible/monitoring/agent.yml +2 -0
- package/ansible/monitoring/hub-darwin.yml +3 -0
- package/ansible/monitoring/hub-route-darwin.yml +1 -0
- package/ansible/net/remote-probe.sh +4 -1
- package/ansible/platforms.json +2 -2
- package/ansible/pyproject.toml +5 -0
- package/ansible/ubuntu/baseline.yml +39 -13
- package/ansible/ubuntu/disk-gc.py +6 -2
- package/ansible/ubuntu/firewall.nft.j2 +50 -0
- package/ansible/ubuntu/policy.json +125 -6
- package/ansible/ubuntu/reboot.yml +89 -0
- package/ansible/ubuntu/recovery.yml +13 -3
- package/ansible/ubuntu/t3/policy.json +3 -0
- package/ansible/ubuntu/t3-desktop.yml +23 -2
- package/ansible/ubuntu/t3-headless.yml +128 -0
- package/ansible/ubuntu/t3-health.sh +24 -10
- package/ansible/ubuntu/t3-idle.py +37 -2
- package/ansible/ubuntu/t3-policy.yml +4 -1
- package/ansible/ubuntu/t3-route.yml +1 -0
- package/ansible/ubuntu/tailscale.yml +54 -8
- package/ansible/ubuntu/tasks/browser.yml +3 -0
- package/ansible/ubuntu/tasks/containers-off.yml +43 -0
- package/ansible/ubuntu/tasks/docker.yml +53 -6
- package/ansible/ubuntu/tasks/dpkg.yml +4 -0
- package/ansible/ubuntu/tasks/features.yml +22 -0
- package/ansible/ubuntu/tasks/firewall.yml +172 -0
- package/ansible/ubuntu/tasks/install.yml +2 -1
- package/ansible/ubuntu/tasks/packages.yml +4 -2
- package/ansible/ubuntu/tasks/release.yml +6 -0
- package/ansible/ubuntu/tasks/resources.yml +6 -4
- package/ansible/ubuntu/tasks/services.yml +2 -2
- package/ansible/ubuntu/tasks/sizing.yml +49 -0
- package/ansible/ubuntu/tasks/snapshot.yml +14 -3
- package/ansible/ubuntu/tasks/ssh.yml +61 -4
- package/ansible/ubuntu/tasks/t3-restart.yml +3 -1
- package/ansible/ubuntu/tasks/user.yml +1 -0
- package/ansible/ubuntu/worker-account.yml +117 -0
- package/ansible/uv.lock +265 -0
- package/bin/devfleet.mjs +42 -4
- package/bootstrap/fleet-remote-bootstrap.sh +91 -25
- package/bootstrap/global-config.ts +29 -3
- package/bootstrap/package-smoke.ts +127 -0
- package/bootstrap/pins.ts +189 -0
- package/bootstrap/release-notes.ts +88 -0
- package/bootstrap/release-version.ts +41 -7
- package/bootstrap/worker-features.ts +114 -0
- package/bootstrap/worker-seed.yaml +65 -0
- package/bootstrap/worker-tools.ts +39 -9
- package/cliproxy/README.md +1 -1
- package/cliproxy/docs/health-workflow.example.yml +20 -143
- package/docs/authentication.md +21 -11
- package/docs/consumer-setup.md +74 -58
- package/docs/convergence.md +67 -0
- package/docs/declared.md +61 -34
- package/docs/machine-setup.md +73 -7
- package/docs/manager-setup.md +34 -0
- package/docs/operations.md +64 -9
- package/docs/releasing.md +49 -1
- package/docs/runbooks.md +112 -7
- package/docs/state.md +23 -11
- package/docs/support-matrix.md +40 -4
- package/docs/topology.md +82 -12
- package/docs/ubuntu-sync.md +98 -42
- package/docs/worker-contract.md +122 -19
- package/docs/worker-requirements.md +107 -0
- package/examples/fleet.json +1 -0
- package/examples/workers-from-infrastructure/main.tf +132 -0
- package/examples/workers-from-infrastructure/worker-records.json +14 -0
- package/llms.txt +3 -1
- package/mise.lock +29 -0
- package/mise.toml +5 -3
- package/package.json +2 -2
- package/schema/check-ids.json +1991 -162
- package/schema/check-ids.schema.json +34 -0
- package/schema/fleet.schema.json +349 -10
- package/schema/output/check.schema.json +1 -1
- package/schema/output/fleet-state-definition.schema.json +14 -0
- package/schema/output/fleet-sync-schedule.schema.json +26 -0
- package/schema/output/fleet-sync.schema.json +18 -0
- package/schema/output/report.schema.json +3 -2
- package/schema/worker-record.schema.json +76 -0
- package/src/agents/converge.ts +140 -24
- package/src/agents/files.ts +2 -32
- package/src/agents/native-verify.ts +34 -3
- package/src/agents/secrets.ts +39 -4
- package/src/check-owners.ts +62 -2
- package/src/check-registry.ts +630 -0
- package/src/cli/agent-guide.ts +3 -3
- package/src/cli/cli.ts +4 -0
- package/src/cli/fleet-manifest.ts +108 -0
- package/src/cli/fleet-node-lifecycle.ts +704 -0
- package/src/cli/fleet-node-park.ts +238 -0
- package/src/cli/fleet-node-seed.ts +167 -0
- package/src/cli/fleet-state.ts +2 -1
- package/src/cli/fleet-sync-checkout.ts +7 -0
- package/src/cli/fleet-sync-enrollment.ts +193 -32
- package/src/cli/fleet-sync-forward.ts +6 -0
- package/src/cli/fleet-sync-manager.ts +123 -8
- package/src/cli/fleet-sync-output.ts +31 -11
- package/src/cli/fleet-sync-plan.ts +65 -9
- package/src/cli/fleet-sync-records.ts +113 -0
- package/src/cli/fleet-sync-remote.ts +172 -15
- package/src/cli/fleet-sync-schedule.ts +14 -4
- package/src/cli/fleet-sync-state.ts +31 -10
- package/src/cli/fleet-sync-vault.ts +152 -26
- package/src/cli/fleet-sync-worker.ts +214 -0
- package/src/cli/fleet-sync.ts +172 -21
- package/src/cli/fleet-validate.ts +277 -0
- package/src/cli/fleet-watch.ts +7 -1
- package/src/cli/fleet.ts +87 -5
- package/src/cli/managed-health.ts +25 -1
- package/src/cli/register-fleet.ts +251 -4
- package/src/cli/registry.ts +47 -0
- package/src/cli/tailnet.ts +43 -10
- package/src/cli/ui-data.ts +3 -1
- package/src/cli/worker-usage-report.ts +63 -0
- package/src/clock.ts +42 -0
- package/src/definition.ts +762 -232
- package/src/diagnostics.ts +5 -2
- package/src/exposure.ts +102 -0
- package/src/fleet-config.ts +3 -1
- package/src/git-identity.ts +76 -3
- package/src/ide/ide.ts +5 -0
- package/src/ide/t3.ts +12 -2
- package/src/input-hashes.ts +6 -0
- package/src/machine/machine-class.ts +59 -0
- package/src/machine/platform.ts +90 -11
- package/src/machine/state.ts +70 -16
- package/src/machine/support-matrix.ts +11 -9
- package/src/machine-health.ts +28 -5
- package/src/manager-health.ts +148 -2
- package/src/manager-sync.ts +89 -8
- package/src/monitoring/beszel.ts +8 -8
- package/src/monitoring/enroll.ts +8 -1
- package/src/monitoring/hub-access.ts +10 -4
- package/src/monitoring/systems.ts +138 -0
- package/src/network-definition.ts +13 -4
- package/src/node-converge-linux.ts +136 -27
- package/src/node-converge.ts +13 -3
- package/src/platform/ansible-receipt.ts +17 -2
- package/src/platform/ansible.ts +40 -4
- package/src/platform/manager-host.ts +25 -8
- package/src/platform/resource-verify.ts +18 -7
- package/src/platform/t3-policy.ts +24 -10
- package/src/platform/t3-service.ts +109 -34
- package/src/platform/ubuntu-release.ts +24 -0
- package/src/platform/ubuntu-verify.ts +31 -3
- package/src/platform/ubuntu-worker.ts +58 -0
- package/src/platform/worker-platform.ts +72 -0
- package/src/protocol.ts +1 -1
- package/src/public-reachability.ts +186 -0
- package/src/remote-bootstrap.ts +50 -12
- package/src/remote-operation.ts +18 -4
- package/src/repair-plan.ts +23 -12
- package/src/report.ts +2 -1
- package/src/schedule-check.ts +21 -2
- package/src/secrets.ts +7 -0
- package/src/service-auth.ts +55 -48
- package/src/ssh-connection.ts +141 -0
- package/src/ssh.ts +63 -24
- package/src/state/store.ts +4 -2
- package/src/state/sync.ts +35 -6
- package/src/state/workers.ts +140 -0
- package/src/t3/pairing.ts +59 -0
- package/src/t3/peer-link.ts +333 -0
- package/src/t3/version.ts +1 -1
- package/src/tailnet-cleanup.ts +185 -0
- package/src/tailnet-control.ts +346 -0
- package/src/tailscale.ts +1 -1
- package/src/toolchain-checks.ts +11 -1
- package/src/types.ts +4 -2
- package/src/worker-account.ts +63 -0
- package/src/worker-activity.ts +303 -0
- package/src/worker-capacity.ts +31 -0
- package/src/worker-credentials.ts +201 -0
- package/src/worker-exposure.ts +388 -0
- package/src/worker-features.ts +201 -0
- package/src/worker-host-checks.ts +71 -20
- package/src/worker-manifest.ts +224 -0
- package/src/worker-parking.ts +70 -0
- package/src/worker-policy.ts +38 -36
- package/src/worker-readiness.ts +15 -3
- package/src/worker-reboot.ts +129 -0
- package/src/worker-records.ts +305 -0
- package/src/worker-seed.ts +143 -0
- package/src/worker-sizing.ts +76 -0
- package/src/cli/schedule-logs.ts +0 -36
|
@@ -5,9 +5,56 @@
|
|
|
5
5
|
"release": "26.04",
|
|
6
6
|
"codename": "resolute",
|
|
7
7
|
"architectures": ["amd64", "arm64"]
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"release": "24.04",
|
|
11
|
+
"codename": "noble",
|
|
12
|
+
"architectures": ["amd64", "arm64"],
|
|
13
|
+
"packages": {
|
|
14
|
+
"sudo": "1.9.15p5-3ubuntu5.24.04.4",
|
|
15
|
+
"openssh-server": "1:9.6p1-3ubuntu13.20",
|
|
16
|
+
"dconf-cli": "0.40.0-4ubuntu0.1",
|
|
17
|
+
"docker.io": "29.1.3-0ubuntu3~24.04.2",
|
|
18
|
+
"docker-compose-v2": "2.40.3+ds1-0ubuntu1~24.04.1",
|
|
19
|
+
"docker-buildx": "0.30.1-0ubuntu1~24.04.1",
|
|
20
|
+
"build-essential": "12.10ubuntu1",
|
|
21
|
+
"ca-certificates": "20260816ubuntu0.24.04.2",
|
|
22
|
+
"curl": "8.5.0-2ubuntu10.16",
|
|
23
|
+
"git": "1:2.43.0-1ubuntu7.4",
|
|
24
|
+
"python3": "3.12.3-0ubuntu2.1",
|
|
25
|
+
"unzip": "6.0-28ubuntu4.1",
|
|
26
|
+
"xz-utils": "5.6.1+really5.4.5-1ubuntu0.4",
|
|
27
|
+
"ansible-core": "2.16.3-0ubuntu2",
|
|
28
|
+
"systemd-oomd": "255.4-1ubuntu8.18",
|
|
29
|
+
"nftables": "1.0.9-1ubuntu0.1"
|
|
30
|
+
},
|
|
31
|
+
"browserPackages": {
|
|
32
|
+
"fonts-liberation": "1:2.1.5-3",
|
|
33
|
+
"libasound2t64": "1.2.11-1ubuntu0.3",
|
|
34
|
+
"libatk-bridge2.0-0t64": "2.52.0-1build1",
|
|
35
|
+
"libatk1.0-0t64": "2.52.0-1build1",
|
|
36
|
+
"libatspi2.0-0t64": "2.52.0-1build1",
|
|
37
|
+
"libcairo2": "1.18.0-3build1",
|
|
38
|
+
"libcups2t64": "2.4.7-1.2ubuntu7.14",
|
|
39
|
+
"libdbus-1-3": "1.14.10-4ubuntu4.1",
|
|
40
|
+
"libdrm2": "2.4.125-1ubuntu0.1~24.04.2",
|
|
41
|
+
"libgbm1": "25.2.8-0ubuntu0.24.04.4",
|
|
42
|
+
"libglib2.0-0t64": "2.80.0-6ubuntu3.10",
|
|
43
|
+
"libnspr4": "2:4.35-1.1build1",
|
|
44
|
+
"libnss3": "2:3.98-1ubuntu0.3",
|
|
45
|
+
"libpango-1.0-0": "1.52.1+ds-1build1",
|
|
46
|
+
"libx11-6": "2:1.8.7-1ubuntu0.1",
|
|
47
|
+
"libxcb1": "1.15-1ubuntu2",
|
|
48
|
+
"libxcomposite1": "1:0.4.5-1build3",
|
|
49
|
+
"libxdamage1": "1:1.1.6-1build1",
|
|
50
|
+
"libxext6": "2:1.3.4-1build2",
|
|
51
|
+
"libxfixes3": "1:6.0.0-2build1",
|
|
52
|
+
"libxkbcommon0": "1.6.0-1build1",
|
|
53
|
+
"libxrandr2": "2:1.5.2-2build1"
|
|
54
|
+
}
|
|
8
55
|
}
|
|
9
56
|
],
|
|
10
|
-
"snapshot": "
|
|
57
|
+
"snapshot": "20261108T000000Z",
|
|
11
58
|
"packages": {
|
|
12
59
|
"sudo": "1.9.17p2-1ubuntu3.2",
|
|
13
60
|
"openssh-server": "1:10.2p1-2ubuntu3.8",
|
|
@@ -23,19 +70,81 @@
|
|
|
23
70
|
"unzip": "6.0-29ubuntu1",
|
|
24
71
|
"xz-utils": "5.8.3-1ubuntu0.1",
|
|
25
72
|
"ansible-core": "2.20.1-1",
|
|
26
|
-
"systemd-oomd": "259.9-0ubuntu0.1"
|
|
73
|
+
"systemd-oomd": "259.9-0ubuntu0.1",
|
|
74
|
+
"nftables": "1.1.6-1"
|
|
27
75
|
},
|
|
28
76
|
"tailscale": {
|
|
29
77
|
"version": "1.104.5",
|
|
30
78
|
"keySha256": "3e03dacf222698c60b8e2f990b809ca1b3e104de127767864284e6c228f1fb39"
|
|
31
79
|
},
|
|
32
80
|
"aptLockTimeoutSeconds": 120,
|
|
33
|
-
"minimumFreeDiskMiB": 4096,
|
|
34
81
|
"minimumFreeInodes": 10000,
|
|
35
82
|
"t3RestartMaxDeferralHours": 24,
|
|
36
83
|
"alwaysOnServices": ["ssh", "tailscaled", "docker", "beszel-agent"],
|
|
84
|
+
"features": {
|
|
85
|
+
"containers": {
|
|
86
|
+
"packages": ["docker.io", "docker-compose-v2", "docker-buildx"],
|
|
87
|
+
"services": ["docker"],
|
|
88
|
+
"groups": ["docker"],
|
|
89
|
+
"tools": ["docker-cli"]
|
|
90
|
+
},
|
|
91
|
+
"desktop": {
|
|
92
|
+
"packages": ["dconf-cli"]
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
"sizing": {
|
|
96
|
+
"minimumWorker": {
|
|
97
|
+
"memoryMiB": 3584,
|
|
98
|
+
"diskMiB": 20480,
|
|
99
|
+
"cpus": 2
|
|
100
|
+
},
|
|
101
|
+
"footprintMiB": {
|
|
102
|
+
"baseTools": 5120,
|
|
103
|
+
"checkouts": 1024,
|
|
104
|
+
"profiles": { "web": 256, "go": 1024, "rust": 2048, "jvm": 640 }
|
|
105
|
+
},
|
|
106
|
+
"agentSessionMiB": 1024,
|
|
107
|
+
"t3ReservedMiB": 1536,
|
|
108
|
+
"thresholds": {
|
|
109
|
+
"swapFile": {
|
|
110
|
+
"of": "memory",
|
|
111
|
+
"share": 0.5,
|
|
112
|
+
"minMiB": 1024,
|
|
113
|
+
"maxMiB": 8192
|
|
114
|
+
},
|
|
115
|
+
"minimumFreeDisk": {
|
|
116
|
+
"of": "disk",
|
|
117
|
+
"share": 0.05,
|
|
118
|
+
"minMiB": 2048,
|
|
119
|
+
"maxMiB": 4096
|
|
120
|
+
},
|
|
121
|
+
"diskGuardFloor": {
|
|
122
|
+
"of": "disk",
|
|
123
|
+
"share": 0.03,
|
|
124
|
+
"minMiB": 1024,
|
|
125
|
+
"maxMiB": 3072
|
|
126
|
+
},
|
|
127
|
+
"diskGcPressure": {
|
|
128
|
+
"of": "disk",
|
|
129
|
+
"share": 0.15,
|
|
130
|
+
"minMiB": 4096,
|
|
131
|
+
"maxMiB": 15360
|
|
132
|
+
},
|
|
133
|
+
"capacityWarn": {
|
|
134
|
+
"of": "disk",
|
|
135
|
+
"share": 0.1,
|
|
136
|
+
"minMiB": 3072,
|
|
137
|
+
"maxMiB": 10240
|
|
138
|
+
},
|
|
139
|
+
"capacityFail": {
|
|
140
|
+
"of": "disk",
|
|
141
|
+
"share": 0.02,
|
|
142
|
+
"minMiB": 1024,
|
|
143
|
+
"maxMiB": 2048
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
},
|
|
37
147
|
"resourceSafety": {
|
|
38
|
-
"swapFileMiB": 8192,
|
|
39
148
|
"userMemoryMax": "90%",
|
|
40
149
|
"agentMemoryMax": "30%",
|
|
41
150
|
"agentSwapMax": "2G",
|
|
@@ -44,11 +153,9 @@
|
|
|
44
153
|
"tmpMaxSize": "25%",
|
|
45
154
|
"tmpAge": "2d",
|
|
46
155
|
"varTmpAge": "14d",
|
|
47
|
-
"diskGuardFloorMiB": 3072,
|
|
48
156
|
"diskGuardWriteMiB": 32,
|
|
49
157
|
"diskGuardIntervalSeconds": 5,
|
|
50
158
|
"diskGcIdleDays": 7,
|
|
51
|
-
"diskGcPressureMiB": 15360,
|
|
52
159
|
"diskGcPressureIdleHours": 12,
|
|
53
160
|
"diskGcDockerAgeHours": 168,
|
|
54
161
|
"diskGcPressureDockerAgeHours": 24,
|
|
@@ -72,6 +179,18 @@
|
|
|
72
179
|
"failOffsetSeconds": 30
|
|
73
180
|
},
|
|
74
181
|
"rebootPendingFailDays": 14,
|
|
182
|
+
"exposure": {
|
|
183
|
+
"tailnetInterface": "tailscale0",
|
|
184
|
+
"tailscalePort": 41641,
|
|
185
|
+
"containerInterfaces": ["docker0", "br-*"],
|
|
186
|
+
"systemListeners": [
|
|
187
|
+
{ "protocol": "udp", "port": 68 },
|
|
188
|
+
{ "protocol": "udp", "port": 546 },
|
|
189
|
+
{ "protocol": "udp", "port": 5353 },
|
|
190
|
+
{ "protocol": "udp", "port": 5355 },
|
|
191
|
+
{ "protocol": "tcp", "port": 5355 }
|
|
192
|
+
]
|
|
193
|
+
},
|
|
75
194
|
"dockerSmokeTest": {
|
|
76
195
|
"image": "busybox:1.37.0@sha256:a7dbc319b1438115a207e11930dc4bb3e643dcb3ec0bdd694bb36cd8fad289ec",
|
|
77
196
|
"timeoutSeconds": 60
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
---
|
|
2
|
+
# One worker rebooted from the manager, only while its T3 runs no agent turn
|
|
3
|
+
# (a reboot ends every running turn), then waited for until it answers again.
|
|
4
|
+
# fleet-sync applies it for a worker's first converge, which nothing can be
|
|
5
|
+
# running on yet, and for --maintenance-reboot, one worker at a time. After
|
|
6
|
+
# it returns, fleet-sync converges the worker again to verify it is ready.
|
|
7
|
+
# Variables: target (the SSH alias, or the declared tailnet name), login
|
|
8
|
+
# (empty for the alias's own user), ssh_options (the SSH options every session
|
|
9
|
+
# of the manager takes, src/ssh-connection.ts), home (the account's home
|
|
10
|
+
# directory),
|
|
11
|
+
# revision (the checkout whose t3-idle.py is asked), reason (first-converge
|
|
12
|
+
# or maintenance) and force (reboot even while agent turns run).
|
|
13
|
+
- name: Address the worker
|
|
14
|
+
hosts: localhost
|
|
15
|
+
connection: local
|
|
16
|
+
gather_facts: false
|
|
17
|
+
tasks:
|
|
18
|
+
- name: Check the request
|
|
19
|
+
ansible.builtin.assert:
|
|
20
|
+
that:
|
|
21
|
+
- "target is match('^[a-zA-Z0-9][a-zA-Z0-9._-]{0,252}$')"
|
|
22
|
+
- ssh_options is string and ssh_options is match('^-o ')
|
|
23
|
+
- "home is match('^/[A-Za-z0-9._/-]{1,4000}$')"
|
|
24
|
+
- "revision is match('^[a-f0-9]{40}$')"
|
|
25
|
+
- reason in ['first-converge', 'maintenance']
|
|
26
|
+
- name: Address the worker by its alias, with strict host keys
|
|
27
|
+
ansible.builtin.add_host:
|
|
28
|
+
name: '{{ target }}'
|
|
29
|
+
groups: [fleet_reboot]
|
|
30
|
+
ansible_python_interpreter: /usr/bin/python3
|
|
31
|
+
ansible_ssh_args: -o ControlMaster=no -o ControlPath=none
|
|
32
|
+
ansible_ssh_common_args: '{{ ssh_options }}'
|
|
33
|
+
worker_home: '{{ home }}'
|
|
34
|
+
worker_revision: '{{ revision }}'
|
|
35
|
+
reboot_reason: '{{ reason }}'
|
|
36
|
+
reboot_force: '{{ force | default(false) | bool }}'
|
|
37
|
+
changed_when: false
|
|
38
|
+
when: login | default('') == ''
|
|
39
|
+
# A worker whose account fleet-sync created this run, over an alias that
|
|
40
|
+
# still logs in as root.
|
|
41
|
+
- name: Address the worker by its alias as the declared account
|
|
42
|
+
ansible.builtin.add_host:
|
|
43
|
+
name: '{{ target }}'
|
|
44
|
+
groups: [fleet_reboot]
|
|
45
|
+
ansible_python_interpreter: /usr/bin/python3
|
|
46
|
+
ansible_ssh_args: -o ControlMaster=no -o ControlPath=none
|
|
47
|
+
ansible_ssh_common_args: '{{ ssh_options }}'
|
|
48
|
+
ansible_user: '{{ login }}'
|
|
49
|
+
worker_home: '{{ home }}'
|
|
50
|
+
worker_revision: '{{ revision }}'
|
|
51
|
+
reboot_reason: '{{ reason }}'
|
|
52
|
+
reboot_force: '{{ force | default(false) | bool }}'
|
|
53
|
+
changed_when: false
|
|
54
|
+
when: login | default('') is match('^[a-z_][a-z0-9_-]{0,31}$')
|
|
55
|
+
|
|
56
|
+
- name: Reboot the worker while it runs no agent turn
|
|
57
|
+
hosts: fleet_reboot
|
|
58
|
+
gather_facts: false
|
|
59
|
+
serial: 1
|
|
60
|
+
tasks:
|
|
61
|
+
- name: Find the state T3 records its runs in
|
|
62
|
+
ansible.builtin.stat:
|
|
63
|
+
path: '{{ worker_home }}/.t3/userdata/statev2.sqlite'
|
|
64
|
+
register: t3_state
|
|
65
|
+
- name: Ask T3 whether an agent turn is running
|
|
66
|
+
ansible.builtin.command:
|
|
67
|
+
argv:
|
|
68
|
+
- python3
|
|
69
|
+
- -I
|
|
70
|
+
- -B
|
|
71
|
+
- '{{ worker_home }}/.local/share/personal/fleet/revisions/{{ worker_revision }}/ansible/ubuntu/t3-idle.py'
|
|
72
|
+
- --state
|
|
73
|
+
- '{{ worker_home }}/.t3/userdata/statev2.sqlite'
|
|
74
|
+
register: t3_idle
|
|
75
|
+
changed_when: false
|
|
76
|
+
check_mode: false
|
|
77
|
+
failed_when: false
|
|
78
|
+
when: t3_state.stat.exists
|
|
79
|
+
# A worker that never ran T3 has no turn to end. One that cannot tell
|
|
80
|
+
# counts as busy: a reboot never ends a turn on a guess.
|
|
81
|
+
- name: Refuse while an agent turn runs
|
|
82
|
+
ansible.builtin.assert:
|
|
83
|
+
that: not t3_state.stat.exists or t3_idle.rc == 0 or reboot_force | bool
|
|
84
|
+
fail_msg: WORKER_REBOOT_BUSY; an agent turn is running on the worker, so it was not rebooted.
|
|
85
|
+
- name: Reboot, and wait for the worker to answer again
|
|
86
|
+
become: true
|
|
87
|
+
ansible.builtin.reboot:
|
|
88
|
+
msg: dev-fleet {{ reboot_reason }} reboot
|
|
89
|
+
reboot_timeout: 900
|
|
@@ -7,11 +7,21 @@
|
|
|
7
7
|
# fleet-repair re-applies this play alone for worker:service-recovery.
|
|
8
8
|
tags: [recovery]
|
|
9
9
|
tasks:
|
|
10
|
-
|
|
10
|
+
# t3_unit is t3code-headless.service on a headless worker.
|
|
11
|
+
- name: Verify the T3 unit runs before adding any recovery trigger
|
|
11
12
|
ansible.builtin.command:
|
|
12
|
-
argv:
|
|
13
|
+
argv:
|
|
14
|
+
[
|
|
15
|
+
systemctl,
|
|
16
|
+
--user,
|
|
17
|
+
is-active,
|
|
18
|
+
"{{ t3_unit | default('t3code-desktop.service') }}",
|
|
19
|
+
]
|
|
20
|
+
become: true
|
|
13
21
|
become_user: '{{ development_user }}'
|
|
14
22
|
environment:
|
|
15
23
|
XDG_RUNTIME_DIR: '/run/user/{{ development_uid }}'
|
|
16
24
|
changed_when: false
|
|
17
|
-
|
|
25
|
+
check_mode: false
|
|
26
|
+
- name: Enable recovery
|
|
27
|
+
ansible.builtin.import_tasks: tasks/recovery.yml
|
|
@@ -52,6 +52,7 @@
|
|
|
52
52
|
argv: [cat, '{{ t3_desktop_dir }}/.personal-fleet-version']
|
|
53
53
|
register: t3_desktop_installed
|
|
54
54
|
changed_when: false
|
|
55
|
+
check_mode: false
|
|
55
56
|
failed_when: false
|
|
56
57
|
- name: Check whether the desktop app is running before an update
|
|
57
58
|
become: true
|
|
@@ -62,6 +63,7 @@
|
|
|
62
63
|
argv: [systemctl, --user, is-active, --quiet, t3code-desktop.service]
|
|
63
64
|
register: t3_desktop_was_active
|
|
64
65
|
changed_when: false
|
|
66
|
+
check_mode: false
|
|
65
67
|
failed_when: false
|
|
66
68
|
- name: Unpack the pinned app when its version differs
|
|
67
69
|
when: t3_desktop_installed.stdout | default('') != t3_desktop_name
|
|
@@ -193,7 +195,8 @@
|
|
|
193
195
|
WantedBy=graphical-session.target
|
|
194
196
|
# Repoint recovery now: a timer still running an older revision's script
|
|
195
197
|
# would restart the headless server right after its approved migration.
|
|
196
|
-
-
|
|
198
|
+
- name: Repoint recovery to this revision
|
|
199
|
+
ansible.builtin.import_tasks: tasks/recovery.yml
|
|
197
200
|
- name: Enable the desktop app for every graphical login
|
|
198
201
|
become: true
|
|
199
202
|
become_user: '{{ development_user }}'
|
|
@@ -204,6 +207,23 @@
|
|
|
204
207
|
name: t3code-desktop.service
|
|
205
208
|
daemon_reload: true
|
|
206
209
|
enabled: true
|
|
210
|
+
# A worker that was a headless worker (t3-headless.yml) keeps its data in
|
|
211
|
+
# ~/.t3; only the server's user service goes.
|
|
212
|
+
- name: Stop the T3 server of a worker that was a headless worker
|
|
213
|
+
become: true
|
|
214
|
+
become_user: '{{ development_user }}'
|
|
215
|
+
environment:
|
|
216
|
+
XDG_RUNTIME_DIR: '/run/user/{{ development_uid }}'
|
|
217
|
+
ansible.builtin.systemd_service:
|
|
218
|
+
scope: user
|
|
219
|
+
name: t3code-headless.service
|
|
220
|
+
state: stopped
|
|
221
|
+
enabled: false
|
|
222
|
+
failed_when: false
|
|
223
|
+
- name: Remove the T3 server's user service
|
|
224
|
+
ansible.builtin.file:
|
|
225
|
+
path: '{{ development_home }}/.config/systemd/user/t3code-headless.service'
|
|
226
|
+
state: absent
|
|
207
227
|
# One T3 process per worker: the desktop app. The former headless server
|
|
208
228
|
# (`t3 service install`) is retired; absent on machines that never had it.
|
|
209
229
|
- name: Retire the former headless T3 server
|
|
@@ -233,7 +253,8 @@
|
|
|
233
253
|
# just installed, only while it runs no agent turn. Otherwise
|
|
234
254
|
# personal-worker-recovery restarts it at the first idle check, and
|
|
235
255
|
# t3:running-version reports T3_RESTART_PENDING until then.
|
|
236
|
-
-
|
|
256
|
+
- name: Restart T3 when it is idle
|
|
257
|
+
ansible.builtin.import_tasks: tasks/t3-restart.yml
|
|
237
258
|
when:
|
|
238
259
|
- t3_desktop_was_active.rc == 0
|
|
239
260
|
- t3_desktop_updated is changed
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
---
|
|
2
|
+
# The worker's only T3 process on a headless worker (fleet.json
|
|
3
|
+
# features.session "headless"): the pinned native T3 server, which the release
|
|
4
|
+
# ships as its own archive beside the desktop package (mise.toml t3_version
|
|
5
|
+
# and t3_linux_*_sha256; bootstrap/native-t3.ts installs it in every worker's
|
|
6
|
+
# toolchain). `t3 serve` runs the same server the desktop app embeds, with no
|
|
7
|
+
# browser, display or GPU. It is a lingering user service: the designated user
|
|
8
|
+
# lingers (tasks/user.yml), so its user manager starts it at boot with nobody
|
|
9
|
+
# logged in. It listens on 127.0.0.1:3773 like the desktop app, so the private
|
|
10
|
+
# route, the policy, the provider instances, the recovery timer and the checks
|
|
11
|
+
# are the same for both session types. ~/.t3 is kept when a worker changes
|
|
12
|
+
# type. Starting or migrating is left to fleet-sync.
|
|
13
|
+
- name: Run the pinned T3 server as a lingering user service
|
|
14
|
+
hosts: localhost
|
|
15
|
+
connection: local
|
|
16
|
+
become: true
|
|
17
|
+
gather_facts: false
|
|
18
|
+
vars_files:
|
|
19
|
+
- policy.json
|
|
20
|
+
tasks:
|
|
21
|
+
- name: Require the pinned server from the worker's toolchain
|
|
22
|
+
ansible.builtin.stat:
|
|
23
|
+
path: '{{ t3_server_path }}'
|
|
24
|
+
register: t3_server_binary
|
|
25
|
+
- name: Refuse to declare a service for a server that is not installed
|
|
26
|
+
ansible.builtin.assert:
|
|
27
|
+
that:
|
|
28
|
+
- t3_server_binary.stat.exists
|
|
29
|
+
- t3_server_binary.stat.executable
|
|
30
|
+
fail_msg: T3_SERVER_INSTALL_FAILED; the pinned T3 server is not installed. Run fleet-sync, which installs the toolchain first.
|
|
31
|
+
- name: Check whether the server is running before an update
|
|
32
|
+
become: true
|
|
33
|
+
become_user: '{{ development_user }}'
|
|
34
|
+
environment:
|
|
35
|
+
XDG_RUNTIME_DIR: '/run/user/{{ development_uid }}'
|
|
36
|
+
ansible.builtin.command:
|
|
37
|
+
argv: [systemctl, --user, is-active, --quiet, t3code-headless.service]
|
|
38
|
+
register: t3_headless_was_active
|
|
39
|
+
changed_when: false
|
|
40
|
+
check_mode: false
|
|
41
|
+
failed_when: false
|
|
42
|
+
# One T3 process per worker: a worker that was a desktop worker keeps its
|
|
43
|
+
# data in ~/.t3; only the app's session service goes.
|
|
44
|
+
- name: Stop the desktop app of a worker that was a desktop worker
|
|
45
|
+
become: true
|
|
46
|
+
become_user: '{{ development_user }}'
|
|
47
|
+
environment:
|
|
48
|
+
XDG_RUNTIME_DIR: '/run/user/{{ development_uid }}'
|
|
49
|
+
ansible.builtin.systemd_service:
|
|
50
|
+
scope: user
|
|
51
|
+
name: t3code-desktop.service
|
|
52
|
+
state: stopped
|
|
53
|
+
enabled: false
|
|
54
|
+
failed_when: false
|
|
55
|
+
- name: Remove the desktop app's session service
|
|
56
|
+
ansible.builtin.file:
|
|
57
|
+
path: '{{ development_home }}/.config/systemd/user/t3code-desktop.service'
|
|
58
|
+
state: absent
|
|
59
|
+
- name: Create the user unit directory
|
|
60
|
+
ansible.builtin.file:
|
|
61
|
+
path: '{{ development_home }}/.config/systemd/user'
|
|
62
|
+
state: directory
|
|
63
|
+
owner: '{{ development_user }}'
|
|
64
|
+
mode: '0700'
|
|
65
|
+
- name: Declare the T3 server as a lingering user service
|
|
66
|
+
ansible.builtin.copy:
|
|
67
|
+
dest: '{{ development_home }}/.config/systemd/user/t3code-headless.service'
|
|
68
|
+
owner: '{{ development_user }}'
|
|
69
|
+
mode: '0600'
|
|
70
|
+
content: |
|
|
71
|
+
# Managed by personal fleet-sync: ansible/ubuntu/t3-headless.yml
|
|
72
|
+
[Unit]
|
|
73
|
+
Description=T3 Code server (headless)
|
|
74
|
+
StartLimitIntervalSec=300
|
|
75
|
+
StartLimitBurst=5
|
|
76
|
+
[Service]
|
|
77
|
+
Type=simple
|
|
78
|
+
WorkingDirectory=%h
|
|
79
|
+
Environment=T3CODE_HOME=%h/.t3
|
|
80
|
+
# /tmp is RAM with a per-user quota that agent and build caches
|
|
81
|
+
# exhaust; agents use disk-backed /var/tmp (aged by tasks/resources.yml).
|
|
82
|
+
Environment=TMPDIR=/var/tmp
|
|
83
|
+
ExecStart="{{ t3_server_path }}" serve --mode web --host 127.0.0.1 --port 3773 --base-dir %h/.t3 --no-browser
|
|
84
|
+
KillMode=mixed
|
|
85
|
+
Restart=always
|
|
86
|
+
RestartSec=5
|
|
87
|
+
CPUWeight=1000
|
|
88
|
+
IOWeight=1000
|
|
89
|
+
MemoryLow={{ sizing.t3ReservedMiB }}M
|
|
90
|
+
OOMScoreAdjust=100
|
|
91
|
+
ManagedOOMPreference=avoid
|
|
92
|
+
[Install]
|
|
93
|
+
WantedBy=default.target
|
|
94
|
+
register: t3_headless_unit
|
|
95
|
+
# Repoint recovery now: a timer still running an older revision's script
|
|
96
|
+
# would act on the unit of the other session type.
|
|
97
|
+
- name: Repoint recovery to this revision
|
|
98
|
+
ansible.builtin.import_tasks: tasks/recovery.yml
|
|
99
|
+
- name: Enable the T3 server for every boot, with nobody logged in
|
|
100
|
+
become: true
|
|
101
|
+
become_user: '{{ development_user }}'
|
|
102
|
+
environment:
|
|
103
|
+
XDG_RUNTIME_DIR: '/run/user/{{ development_uid }}'
|
|
104
|
+
ansible.builtin.systemd_service:
|
|
105
|
+
scope: user
|
|
106
|
+
name: t3code-headless.service
|
|
107
|
+
daemon_reload: true
|
|
108
|
+
enabled: true
|
|
109
|
+
- name: Start the T3 server without restarting a running one
|
|
110
|
+
become: true
|
|
111
|
+
become_user: '{{ development_user }}'
|
|
112
|
+
environment:
|
|
113
|
+
XDG_RUNTIME_DIR: '/run/user/{{ development_uid }}'
|
|
114
|
+
ansible.builtin.systemd_service:
|
|
115
|
+
scope: user
|
|
116
|
+
name: t3code-headless.service
|
|
117
|
+
state: started
|
|
118
|
+
# A running server keeps the files of its release: restart it into the
|
|
119
|
+
# one just declared, only while it runs no agent turn (as the desktop
|
|
120
|
+
# app). Otherwise personal-worker-recovery restarts it at the first idle
|
|
121
|
+
# check, and t3:running-version reports T3_RESTART_PENDING until then.
|
|
122
|
+
- name: Restart T3 when it is idle
|
|
123
|
+
ansible.builtin.import_tasks: tasks/t3-restart.yml
|
|
124
|
+
vars:
|
|
125
|
+
t3_unit: t3code-headless.service
|
|
126
|
+
when:
|
|
127
|
+
- t3_headless_was_active.rc == 0
|
|
128
|
+
- t3_headless_unit is changed
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
#!/bin/sh
|
|
2
|
-
# Keep the worker T3
|
|
3
|
-
#
|
|
4
|
-
# (
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
# -
|
|
8
|
-
#
|
|
9
|
-
#
|
|
2
|
+
# Keep the worker's T3 up, responsive and on the installed release: the
|
|
3
|
+
# desktop app (UI and server) on a desktop worker, the native server
|
|
4
|
+
# (t3code-headless.service, t3-headless.yml) on a headless one. Run every
|
|
5
|
+
# minute by personal-worker-recovery.timer (managed by fleet-sync,
|
|
6
|
+
# tasks/recovery.yml), from the stable copy the role installs beside
|
|
7
|
+
# t3-idle.py.
|
|
8
|
+
# - The former upstream server unit (t3code.service) is stopped and
|
|
9
|
+
# disabled: one T3 per worker.
|
|
10
|
+
# - Desktop only. No graphical session: T3_NO_GRAPHICAL_SESSION; after three
|
|
11
|
+
# checks in a row the display manager is restarted so auto-login starts
|
|
12
|
+
# one, three times at most until a session is seen again. A headless
|
|
13
|
+
# worker has no session to wait for.
|
|
10
14
|
# - Stopped or failed: reset its start limit and start it.
|
|
11
15
|
# - Running but its local API has not answered for three checks in a row:
|
|
12
16
|
# restart it, since a hung server serves no turns.
|
|
@@ -15,6 +19,11 @@
|
|
|
15
19
|
set -u
|
|
16
20
|
url=http://127.0.0.1:3773/.well-known/t3/environment
|
|
17
21
|
unit=t3code-desktop.service
|
|
22
|
+
headless=
|
|
23
|
+
if systemctl --user is-enabled --quiet t3code-headless.service; then
|
|
24
|
+
unit=t3code-headless.service
|
|
25
|
+
headless=1
|
|
26
|
+
fi
|
|
18
27
|
runtime="${XDG_RUNTIME_DIR:?}"
|
|
19
28
|
failures="$runtime/personal-t3-health-failures"
|
|
20
29
|
no_session="$runtime/personal-t3-no-session"
|
|
@@ -40,7 +49,7 @@ if systemctl --user is-active --quiet t3code.service ||
|
|
|
40
49
|
fi
|
|
41
50
|
fi
|
|
42
51
|
|
|
43
|
-
if ! systemctl --user is-active --quiet graphical-session.target; then
|
|
52
|
+
if [ -z "$headless" ] && ! systemctl --user is-active --quiet graphical-session.target; then
|
|
44
53
|
rm -f "$failures"
|
|
45
54
|
seen=$(count "$no_session")
|
|
46
55
|
echo "T3_NO_GRAPHICAL_SESSION ($seen/$limit)."
|
|
@@ -78,7 +87,12 @@ fi
|
|
|
78
87
|
rm -f "$failures"
|
|
79
88
|
|
|
80
89
|
running=$(printf '%s' "$environment" | sed -n 's/.*"serverVersion" *: *"\([^"]*\)".*/\1/p')
|
|
81
|
-
|
|
90
|
+
if [ -n "$headless" ]; then
|
|
91
|
+
# The release is the versioned directory the unit runs the server from.
|
|
92
|
+
release=$(systemctl --user show --property=ExecStart --value "$unit" | sed -n 's|.*/t3-\([^/ ]*\)-linux-[a-z0-9]*/t3 .*|\1|p' | head -n 1)
|
|
93
|
+
else
|
|
94
|
+
release=$(sed -n 's/^T3-Code-\(.*\)-[a-z0-9]*\.deb$/\1/p' "$installed" 2>/dev/null)
|
|
95
|
+
fi
|
|
82
96
|
reason=
|
|
83
97
|
[ -f "$pending" ] && reason="the staged policy"
|
|
84
98
|
if [ -n "$release" ] && [ "$running" != "$release" ]; then
|
|
@@ -5,17 +5,29 @@ T3 offers no command that lists the running turns of every project, so this
|
|
|
5
5
|
reads the runs T3 records in its own state database, read-only. Anything it
|
|
6
6
|
cannot read counts as busy: a restart never ends a turn on a guess.
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
With --activity it prints one JSON line for fleet-status instead and exits 0:
|
|
9
|
+
how many agent turns are under way and when the last one was requested or
|
|
10
|
+
ended. Only the status and the times of a run are read, never its content.
|
|
11
|
+
|
|
12
|
+
Usage: t3-idle.py --state ~/.t3/userdata/statev2.sqlite [--activity]
|
|
9
13
|
"""
|
|
10
14
|
import argparse
|
|
15
|
+
import json
|
|
11
16
|
import sqlite3
|
|
12
17
|
import sys
|
|
13
18
|
|
|
14
19
|
IDLE, BUSY, UNKNOWN = 0, 1, 2
|
|
15
20
|
|
|
21
|
+
# The statuses of a turn that has not ended yet.
|
|
22
|
+
UNDER_WAY = ('queued', 'preparing', 'starting', 'running', 'waiting')
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def connect(state):
|
|
26
|
+
return sqlite3.connect(f'file:{state}?mode=ro', uri=True, timeout=5)
|
|
27
|
+
|
|
16
28
|
|
|
17
29
|
def running_turns(state):
|
|
18
|
-
connection =
|
|
30
|
+
connection = connect(state)
|
|
19
31
|
try:
|
|
20
32
|
(count,) = connection.execute(
|
|
21
33
|
"SELECT count(*) FROM orchestration_v2_projection_runs WHERE status = 'running'"
|
|
@@ -25,10 +37,33 @@ def running_turns(state):
|
|
|
25
37
|
connection.close()
|
|
26
38
|
|
|
27
39
|
|
|
40
|
+
def activity(state):
|
|
41
|
+
connection = connect(state)
|
|
42
|
+
try:
|
|
43
|
+
marks = ','.join('?' for _ in UNDER_WAY)
|
|
44
|
+
(under_way,) = connection.execute(
|
|
45
|
+
f'SELECT count(*) FROM orchestration_v2_projection_runs WHERE status IN ({marks})',
|
|
46
|
+
UNDER_WAY,
|
|
47
|
+
).fetchone()
|
|
48
|
+
(last,) = connection.execute(
|
|
49
|
+
'SELECT max(coalesce(completed_at, requested_at)) FROM orchestration_v2_projection_runs'
|
|
50
|
+
).fetchone()
|
|
51
|
+
return {'version': 1, 'running': under_way, 'lastActivity': last}
|
|
52
|
+
finally:
|
|
53
|
+
connection.close()
|
|
54
|
+
|
|
55
|
+
|
|
28
56
|
def main():
|
|
29
57
|
parser = argparse.ArgumentParser()
|
|
30
58
|
parser.add_argument('--state', required=True)
|
|
59
|
+
parser.add_argument('--activity', action='store_true')
|
|
31
60
|
args = parser.parse_args()
|
|
61
|
+
if args.activity:
|
|
62
|
+
try:
|
|
63
|
+
print(json.dumps(activity(args.state)))
|
|
64
|
+
except sqlite3.Error:
|
|
65
|
+
print(json.dumps({'version': 1, 'unknown': True}))
|
|
66
|
+
return 0
|
|
32
67
|
try:
|
|
33
68
|
count = running_turns(args.state)
|
|
34
69
|
except sqlite3.Error:
|
|
@@ -53,6 +53,7 @@
|
|
|
53
53
|
argv: [systemctl, --user, is-active, --quiet, t3code-desktop.service]
|
|
54
54
|
register: t3_desktop_active
|
|
55
55
|
changed_when: false
|
|
56
|
+
check_mode: false
|
|
56
57
|
failed_when: false
|
|
57
58
|
- name: Ask the running T3 which policy it loaded
|
|
58
59
|
become: true
|
|
@@ -61,9 +62,11 @@
|
|
|
61
62
|
argv: ['{{ t3_executable }}', policy, show]
|
|
62
63
|
register: t3_policy_loaded
|
|
63
64
|
changed_when: false
|
|
65
|
+
check_mode: false
|
|
64
66
|
failed_when: false
|
|
65
67
|
when: t3_desktop_active.rc == 0
|
|
66
|
-
-
|
|
68
|
+
- name: Restart T3 when it is idle
|
|
69
|
+
ansible.builtin.import_tasks: tasks/t3-restart.yml
|
|
67
70
|
when:
|
|
68
71
|
- t3_desktop_active.rc == 0
|
|
69
72
|
- t3_policy_loaded.stdout is not search('loaded this policy\.\s*$')
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
argv: [tailscale, serve, status, --json]
|
|
18
18
|
register: t3_serve_status
|
|
19
19
|
changed_when: false
|
|
20
|
+
check_mode: false
|
|
20
21
|
- name: Publish T3 on tailnet HTTPS port 443
|
|
21
22
|
ansible.builtin.command:
|
|
22
23
|
argv: [tailscale, serve, --bg, --https=443, '{{ t3_route_target }}']
|