@microck/raft 0.0.0-stage → 0.2.1

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.
Files changed (67) hide show
  1. package/.github/assets/raft-logo.png +0 -0
  2. package/LICENSE +21 -0
  3. package/README.md +167 -2
  4. package/deploy/benchmark-boot.py +150 -0
  5. package/deploy/build-image.py +95 -0
  6. package/deploy/deploy.py +176 -0
  7. package/deploy/incus-network.conf +4 -0
  8. package/deploy/install-incus.sh +22 -0
  9. package/deploy/raft-expire.py +62 -0
  10. package/deploy/raft-expire.service +8 -0
  11. package/deploy/raft-expire.timer +8 -0
  12. package/deploy/raft-network.service +12 -0
  13. package/deploy/raft-network.sh +40 -0
  14. package/deploy/verify-controls.py +241 -0
  15. package/deploy/verify-features.py +519 -0
  16. package/deploy/verify-fresh-host.py +301 -0
  17. package/deploy/verify-image-tests.py +139 -0
  18. package/deploy/verify-image.py +95 -0
  19. package/deploy/verify-limits.py +110 -0
  20. package/deploy/verify-live.py +903 -0
  21. package/deploy/verify-release.py +132 -0
  22. package/deploy/verify-tools.py +105 -0
  23. package/docs/benchmarks/amd64-native-ci.json +137 -0
  24. package/docs/benchmarks/arm64-baseline-a.json +108 -0
  25. package/docs/benchmarks/arm64-baseline-b.json +108 -0
  26. package/docs/benchmarks/arm64-candidate-a.json +109 -0
  27. package/docs/benchmarks/arm64-desktop-local.json +22 -0
  28. package/docs/benchmarks/arm64-native-ci.json +137 -0
  29. package/docs/boat-capabilities.md +64 -0
  30. package/docs/boat-parity-design.md +26 -0
  31. package/docs/boot-performance.mdx +102 -0
  32. package/docs/contract.md +93 -0
  33. package/docs/first-box.mdx +86 -0
  34. package/docs/guides/background-jobs.mdx +95 -0
  35. package/docs/guides/backup-recovery.mdx +111 -0
  36. package/docs/guides/desktop-services.mdx +90 -0
  37. package/docs/guides/docker.mdx +84 -0
  38. package/docs/guides/files.mdx +91 -0
  39. package/docs/guides/long-running-services.mdx +109 -0
  40. package/docs/guides/meta.json +18 -0
  41. package/docs/guides/project-setup.mdx +112 -0
  42. package/docs/guides/scripting-ci.mdx +133 -0
  43. package/docs/guides/snapshots-forks.mdx +107 -0
  44. package/docs/guides/troubleshooting.mdx +152 -0
  45. package/docs/guides/upgrades.mdx +113 -0
  46. package/docs/incus.example.json +3 -0
  47. package/docs/index.mdx +40 -0
  48. package/docs/meta.json +21 -0
  49. package/docs/operations.mdx +239 -0
  50. package/docs/publishing.md +48 -0
  51. package/docs/reference/capacity-limits.mdx +92 -0
  52. package/docs/reference/cli.md +288 -0
  53. package/docs/reference/configuration.mdx +77 -0
  54. package/docs/reference/meta.json +10 -0
  55. package/docs/releases/0.2.0.md +22 -0
  56. package/docs/releases/0.2.1.md +14 -0
  57. package/docs/requirements.mdx +47 -0
  58. package/docs/reviews/code-review-2026-10-06.md +168 -0
  59. package/docs/site-development.md +80 -0
  60. package/docs/verification.mdx +237 -0
  61. package/images/incus/build.sh +57 -0
  62. package/images/incus/desktop.sh +21 -0
  63. package/images/incus/raft-desktop.service +7 -0
  64. package/package.json +46 -4
  65. package/raft.py +961 -0
  66. package/raft_files.py +134 -0
  67. package/skills/raft-cli/SKILL.md +117 -0
@@ -0,0 +1,301 @@
1
+ """Provision and reboot a disposable KVM Ubuntu host using the canonical deployer.
2
+
3
+ CI-only harness. Writes temporary verified SSH/configuration in its own HOME.
4
+ Uses a sparse 100 GiB virtual disk, not an 80 GiB physical capacity guarantee.
5
+ """
6
+
7
+ import argparse
8
+ import hashlib
9
+ import json
10
+ import os
11
+ from pathlib import Path
12
+ import subprocess
13
+ import sys
14
+ import tempfile
15
+ import time
16
+ import urllib.request
17
+
18
+ ROOT = Path(__file__).resolve().parent.parent
19
+ USER_AGENT = "OpenAI File Downloader, XaiImageApiFetch/1.0"
20
+ BASE = "https://cloud-images.ubuntu.com/noble/current/"
21
+
22
+
23
+ def run(*args, **kwargs):
24
+ return subprocess.run(args, check=True, **kwargs)
25
+
26
+
27
+ def verify_firewall(env):
28
+ """Disrupt only this harness's disposable VM to prove the startup dependency."""
29
+ ssh = ["ssh", "raft-fresh"]
30
+ requires = subprocess.check_output(
31
+ [*ssh, "systemctl show incus.service -p Requires -p After"], env=env, text=True
32
+ )
33
+ assert all(
34
+ "raft-network.service" in line.split("=", 1)[1].split() for line in requires.splitlines()
35
+ )
36
+ started = subprocess.check_output(
37
+ [*ssh, "systemctl show incus.service -p ExecMainStartTimestampMonotonic --value"],
38
+ env=env,
39
+ text=True,
40
+ )
41
+ # Deliberately inject an obsolete allowance with no boxes present, then redeploy.
42
+ run(
43
+ *ssh,
44
+ "sudo iptables -N RAFT-VERIFY-UNRELATED && "
45
+ "sudo iptables -A RAFT-VERIFY-UNRELATED -j RETURN && "
46
+ "sudo iptables -I RAFT-FORWARD 1 -d 169.254.0.0/16 -j ACCEPT",
47
+ env=env,
48
+ )
49
+ run(sys.executable, str(ROOT / "deploy/deploy.py"), "--location", "lab", env=env)
50
+ run(
51
+ *ssh,
52
+ "sudo iptables -C RAFT-FORWARD -d 169.254.0.0/16 -j REJECT && "
53
+ "! sudo iptables -C RAFT-FORWARD -d 169.254.0.0/16 -j ACCEPT && "
54
+ "sudo iptables -C RAFT-VERIFY-UNRELATED -j RETURN && "
55
+ "sudo iptables -F RAFT-VERIFY-UNRELATED && sudo iptables -X RAFT-VERIFY-UNRELATED",
56
+ env=env,
57
+ )
58
+ unchanged = subprocess.check_output(
59
+ [*ssh, "systemctl show incus.service -p ExecMainStartTimestampMonotonic --value"],
60
+ env=env,
61
+ text=True,
62
+ )
63
+ assert started == unchanged, "Firewall reload restarted the Incus daemon"
64
+ run(*ssh, "sudo cp /usr/local/lib/raft/network /tmp/raft-network-proof", env=env)
65
+ try:
66
+ run(
67
+ *ssh,
68
+ "sudo systemctl stop raft-expire.timer incus-startup.service incus.socket incus.service raft-network.service",
69
+ env=env,
70
+ )
71
+ run(
72
+ *ssh,
73
+ "printf '#!/bin/sh\\nexit 1\\n' | sudo tee /usr/local/lib/raft/network >/dev/null",
74
+ env=env,
75
+ )
76
+ failed = subprocess.run(
77
+ [*ssh, "sudo systemctl start incus.service"], env=env, capture_output=True, text=True
78
+ )
79
+ assert failed.returncode != 0, "Incus started despite a failed firewall dependency"
80
+ active = subprocess.run(
81
+ [*ssh, "systemctl is-active incus.service"], env=env, capture_output=True, text=True
82
+ )
83
+ assert active.stdout.strip() != "active"
84
+ run(*ssh, "sudo systemctl start incus.socket", env=env)
85
+ failed = subprocess.run(
86
+ [*ssh, "sudo timeout 20 incus query /1.0"], env=env, capture_output=True, text=True
87
+ )
88
+ assert failed.returncode != 0, "Socket activation bypassed the firewall dependency"
89
+ active = subprocess.run(
90
+ [*ssh, "systemctl is-active incus.service"], env=env, capture_output=True, text=True
91
+ )
92
+ assert active.stdout.strip() != "active"
93
+ finally:
94
+ run(
95
+ *ssh,
96
+ "sudo cp /tmp/raft-network-proof /usr/local/lib/raft/network && sudo rm /tmp/raft-network-proof && "
97
+ "sudo systemctl reset-failed incus.service raft-network.service && "
98
+ "sudo systemctl reload-or-restart raft-network.service && "
99
+ "sudo systemctl start incus.socket incus.service incus-startup.service raft-expire.timer",
100
+ env=env,
101
+ )
102
+ print(
103
+ "Repeat deploy removed stale rules without restarting Incus; firewall failure blocked normal and socket startup",
104
+ flush=True,
105
+ )
106
+
107
+
108
+ def verify(archive):
109
+ # Fail rather than silently switching to software emulation.
110
+ if not os.access("/dev/kvm", os.R_OK | os.W_OK):
111
+ raise RuntimeError("Fresh-host verification requires accessible KVM")
112
+ with tempfile.TemporaryDirectory(prefix="raft-fresh-") as directory:
113
+ work = Path(directory)
114
+ home = work / "controller"
115
+ ssh = home / ".ssh"
116
+ config = home / ".config/raft"
117
+ ssh.mkdir(parents=True, mode=0o700)
118
+ config.mkdir(parents=True, mode=0o700)
119
+ for key in ["client", "host"]:
120
+ run("ssh-keygen", "-q", "-t", "ed25519", "-N", "", "-f", str(work / key))
121
+ (ssh / "config").write_text(
122
+ f"Host raft-fresh\n HostName localhost\n Port 2222\n User raftci\n"
123
+ f" IdentityFile {work / 'client'}\n IdentitiesOnly yes\n"
124
+ f" UserKnownHostsFile {ssh / 'known_hosts'}\n StrictHostKeyChecking yes\n"
125
+ )
126
+ (ssh / "known_hosts").write_text("[localhost]:2222 " + (work / "host.pub").read_text())
127
+ configuration = config / "incus.json"
128
+ configuration.write_text(json.dumps({"lab": {"ssh": "raft-fresh", "image": ""}}))
129
+ configuration.chmod(0o600)
130
+ # A per-harness HOME keeps the operator's controller config untouched.
131
+ env = {**os.environ, "HOME": str(home)}
132
+ cloud = {
133
+ "hostname": "raft-fresh",
134
+ "users": [
135
+ {
136
+ "name": "raftci",
137
+ "sudo": "ALL=(ALL) NOPASSWD:ALL",
138
+ "shell": "/bin/bash",
139
+ "ssh_authorized_keys": [(work / "client.pub").read_text().strip()],
140
+ }
141
+ ],
142
+ "ssh_pwauth": False,
143
+ "ssh_keys": {
144
+ "ed25519_private": (work / "host").read_text(),
145
+ "ed25519_public": (work / "host.pub").read_text(),
146
+ },
147
+ "packages": ["curl", "gnupg", "btrfs-progs"],
148
+ }
149
+ (work / "user-data").write_text("#cloud-config\n" + json.dumps(cloud))
150
+ (work / "meta-data").write_text("instance-id: raft-fresh\nlocal-hostname: raft-fresh\n")
151
+ image = work / "noble-server-cloudimg-amd64.img"
152
+ request = urllib.request.Request(BASE + "SHA256SUMS", headers={"User-Agent": USER_AGENT})
153
+ sums = urllib.request.urlopen(request, timeout=60).read().decode()
154
+ checksum = next(
155
+ line.split()[0]
156
+ for line in sums.splitlines()
157
+ if line.split()[-1].lstrip("*") == image.name
158
+ )
159
+ run("curl", "-fsSL", "-A", USER_AGENT, BASE + image.name, "-o", str(image))
160
+ with image.open("rb") as downloaded:
161
+ assert hashlib.file_digest(downloaded, "sha256").hexdigest() == checksum
162
+ run("qemu-img", "resize", str(image), "100G")
163
+ run(
164
+ "cloud-localds",
165
+ str(work / "seed.img"),
166
+ str(work / "user-data"),
167
+ str(work / "meta-data"),
168
+ )
169
+ with (work / "serial.log").open("wb") as serial:
170
+ vm = subprocess.Popen(
171
+ [
172
+ "qemu-system-x86_64",
173
+ "-enable-kvm",
174
+ "-cpu",
175
+ "host",
176
+ "-smp",
177
+ "2",
178
+ "-m",
179
+ "8192",
180
+ "-display",
181
+ "none",
182
+ "-serial",
183
+ "stdio",
184
+ "-monitor",
185
+ "none",
186
+ "-drive",
187
+ f"file={image},format=qcow2,if=virtio",
188
+ "-drive",
189
+ f"file={work / 'seed.img'},format=raw,if=virtio",
190
+ "-netdev",
191
+ "user,id=net,hostfwd=tcp:127.0.0.1:2222-:22",
192
+ "-device",
193
+ "virtio-net-pci,netdev=net",
194
+ ],
195
+ stdout=serial,
196
+ stderr=subprocess.STDOUT,
197
+ )
198
+ try:
199
+ # SSH expands ~ using passwd, not HOME. Pass the fixture config explicitly
200
+ # through a PATH wrapper shared by CLI SSH and SCP calls.
201
+ binaries = work / "bin"
202
+ binaries.mkdir()
203
+ for tool in ["ssh", "scp"]:
204
+ (binaries / tool).write_text(
205
+ f'#!/bin/sh\nexec /usr/bin/{tool} -F "{ssh / "config"}" "$@"\n'
206
+ )
207
+ (binaries / tool).chmod(0o755)
208
+ env["PATH"] = str(binaries) + os.pathsep + env["PATH"]
209
+ deadline = time.monotonic() + 300
210
+ while time.monotonic() < deadline:
211
+ if vm.poll() is not None:
212
+ raise RuntimeError("Fresh VM exited during boot")
213
+ probe = subprocess.run(
214
+ [
215
+ "ssh",
216
+ "-T",
217
+ "-o",
218
+ "BatchMode=yes",
219
+ "-o",
220
+ "ConnectTimeout=2",
221
+ "raft-fresh",
222
+ "true",
223
+ ],
224
+ env=env,
225
+ capture_output=True,
226
+ )
227
+ if probe.returncode == 0:
228
+ break
229
+ time.sleep(2)
230
+ else:
231
+ raise RuntimeError("Fresh VM SSH did not become ready")
232
+ run("ssh", "raft-fresh", "sudo cloud-init status --wait --long", env=env)
233
+ run(sys.executable, str(ROOT / "deploy/deploy.py"), "--location", "lab", env=env)
234
+ verify_firewall(env)
235
+ run("scp", str(archive), "raft-fresh:/tmp/raft-image.tar.gz", env=env)
236
+ run(
237
+ "ssh",
238
+ "raft-fresh",
239
+ "sudo incus --project raft image import /tmp/raft-image.tar.gz --alias raft-dev",
240
+ env=env,
241
+ )
242
+ images = subprocess.check_output(
243
+ [
244
+ "ssh",
245
+ "raft-fresh",
246
+ "sudo incus --project raft image list raft-dev --format json",
247
+ ],
248
+ env=env,
249
+ text=True,
250
+ )
251
+ fingerprint = next(
252
+ item["fingerprint"]
253
+ for item in json.loads(images)
254
+ if any(a["name"] == "raft-dev" for a in item["aliases"])
255
+ )
256
+ configuration.write_text(
257
+ json.dumps({"lab": {"ssh": "raft-fresh", "image": fingerprint}})
258
+ )
259
+ run("ssh", "raft-fresh", "rm /tmp/raft-image.tar.gz", env=env)
260
+ run(
261
+ sys.executable,
262
+ str(ROOT / "deploy/verify-live.py"),
263
+ "--location",
264
+ "lab",
265
+ "--reboot-host",
266
+ env=env,
267
+ )
268
+ # Oversize only this disposable pool after all boxes have been removed.
269
+ run("ssh", "raft-fresh", "sudo incus storage set raft-data size=61GiB", env=env)
270
+ drift = subprocess.run(
271
+ [sys.executable, str(ROOT / "deploy/deploy.py"), "--location", "lab"],
272
+ env=env,
273
+ capture_output=True,
274
+ text=True,
275
+ )
276
+ assert drift.returncode != 0 and "size=60GiB" in drift.stderr
277
+ size = subprocess.check_output(
278
+ ["ssh", "raft-fresh", "sudo incus storage get raft-data size"],
279
+ env=env,
280
+ text=True,
281
+ )
282
+ assert size.strip() == "61GiB", "Deployment silently resized a drifting pool"
283
+ print(
284
+ "Fresh provisioning, firewall failure gating, redeployment, oversize rejection and real host reboot passed",
285
+ flush=True,
286
+ )
287
+ finally:
288
+ vm.terminate()
289
+ vm.wait(timeout=30)
290
+ # Report diagnostics only on failure; never publish fixture keys.
291
+ if sys.exc_info()[0]:
292
+ print(
293
+ (work / "serial.log").read_text(errors="replace")[-10000:], file=sys.stderr
294
+ )
295
+
296
+
297
+ if __name__ == "__main__":
298
+ parser = argparse.ArgumentParser(description=__doc__)
299
+ parser.add_argument("archive", type=Path)
300
+ args = parser.parse_args()
301
+ verify(args.archive.resolve())
@@ -0,0 +1,139 @@
1
+ """Exercise the real image inspector with clean and credential-bearing tar archives."""
2
+
3
+ import hashlib
4
+ import io
5
+ import json
6
+ import os
7
+ from pathlib import Path, PurePosixPath
8
+ import subprocess
9
+ import sys
10
+ import tarfile
11
+ import tempfile
12
+
13
+ ROOT = Path(__file__).resolve().parent.parent
14
+
15
+
16
+ def verify():
17
+ with tempfile.TemporaryDirectory(prefix="raft-image-check-") as directory:
18
+ work = Path(directory)
19
+ key = work / "key"
20
+ subprocess.run(["ssh-keygen", "-q", "-t", "ed25519", "-N", "", "-f", str(key)], check=True)
21
+ home = PurePosixPath("rootfs", "home", "developer")
22
+ root = PurePosixPath("rootfs", "root")
23
+ cases = [
24
+ ("clean", None, tarfile.REGTYPE, "", True),
25
+ ("user-key", home / ".ssh/id_ed25519", tarfile.REGTYPE, "", False),
26
+ ("root-key", root / ".ssh/id_ed25519", tarfile.REGTYPE, "", False),
27
+ ("user-key-link", home / ".ssh/id_ed25519", tarfile.SYMTYPE, "/etc/passwd", False),
28
+ ("ssh-directory-link", home / ".ssh", tarfile.SYMTYPE, "/etc", False),
29
+ (
30
+ "user-key-hardlink",
31
+ home / ".ssh/id_ed25519",
32
+ tarfile.LNKTYPE,
33
+ "metadata.yaml",
34
+ False,
35
+ ),
36
+ ("user-key-fifo", home / ".ssh/id_ed25519", tarfile.FIFOTYPE, "", False),
37
+ ("key-target-link", home / "alias", tarfile.SYMTYPE, ".ssh/id_ed25519", False),
38
+ (
39
+ "key-target-hardlink",
40
+ home / "alias",
41
+ tarfile.LNKTYPE,
42
+ str(home / ".ssh/id_ed25519"),
43
+ False,
44
+ ),
45
+ (
46
+ "host-key-link",
47
+ PurePosixPath("rootfs/etc/ssh/ssh_host_ed25519_key"),
48
+ tarfile.SYMTYPE,
49
+ "/etc/passwd",
50
+ False,
51
+ ),
52
+ ("npm-token", home / ".npmrc", tarfile.REGTYPE, "", False),
53
+ ("empty-ssh-directory", home / ".ssh", tarfile.DIRTYPE, "", True),
54
+ (
55
+ "package-config",
56
+ PurePosixPath("rootfs/usr/lib/node_modules/npm/.npmrc"),
57
+ tarfile.REGTYPE,
58
+ "",
59
+ True,
60
+ ),
61
+ (
62
+ "package-token",
63
+ PurePosixPath("rootfs/usr/lib/node_modules/npm/npmrc"),
64
+ tarfile.REGTYPE,
65
+ "",
66
+ False,
67
+ ),
68
+ (
69
+ "normal-link",
70
+ PurePosixPath("rootfs/usr/bin/example"),
71
+ tarfile.SYMTYPE,
72
+ "../lib/example",
73
+ True,
74
+ ),
75
+ ]
76
+ for label, path, kind, target, accepted in cases:
77
+ case = work / label
78
+ case.mkdir()
79
+ archive = case / "image.tar.gz"
80
+ resources = {
81
+ "metadata.yaml": b"architecture: aarch64\n",
82
+ "rootfs/etc/machine-id": b"",
83
+ "rootfs/opt/raft/packages.tsv": b"example\t1\n",
84
+ "rootfs/opt/raft/npm-packages.json": b"{}\n",
85
+ }
86
+ with tarfile.open(archive, "w:gz") as output:
87
+ for name, content in resources.items():
88
+ member = tarfile.TarInfo(name)
89
+ member.size = len(content)
90
+ output.addfile(member, io.BytesIO(content))
91
+ if path:
92
+ member = tarfile.TarInfo(str(path))
93
+ member.type = kind
94
+ member.linkname = target
95
+ content = (
96
+ key.read_bytes()
97
+ if "key" in label
98
+ else b"registry=https://registry.npmjs.org/\n"
99
+ )
100
+ if label in {"npm-token", "package-token"}:
101
+ content = b"//registry.npmjs.org/:_authToken=fixture-token\n"
102
+ if kind == tarfile.REGTYPE:
103
+ member.size = len(content)
104
+ output.addfile(member, io.BytesIO(content) if kind == tarfile.REGTYPE else None)
105
+ fingerprint = hashlib.sha256(archive.read_bytes()).hexdigest()
106
+ response = subprocess.run(
107
+ [
108
+ sys.executable,
109
+ str(ROOT / "deploy/verify-image.py"),
110
+ str(archive),
111
+ "--architecture",
112
+ "arm64",
113
+ "--fingerprint",
114
+ fingerprint,
115
+ ],
116
+ capture_output=True,
117
+ text=True,
118
+ env={
119
+ **os.environ,
120
+ "GITHUB_SHA": "0" * 40,
121
+ "GITHUB_REPOSITORY": "Microck/raft",
122
+ "GITHUB_RUN_ID": "0",
123
+ },
124
+ )
125
+ assert (response.returncode == 0) == accepted, (label, response.stderr)
126
+ if accepted:
127
+ manifest = json.loads((case / "manifest-arm64.json").read_text())
128
+ assert manifest["sha256"] == fingerprint
129
+ assert (case / "packages-arm64.tsv").read_bytes() == resources[
130
+ "rootfs/opt/raft/packages.tsv"
131
+ ]
132
+ else:
133
+ assert "credential" in response.stderr.lower(), (label, response.stderr)
134
+ assert not (case / "manifest-arm64.json").exists()
135
+ print(f"Image inspector: {len(cases)} real clean/credential archive cases passed")
136
+
137
+
138
+ if __name__ == "__main__":
139
+ verify()
@@ -0,0 +1,95 @@
1
+ """Inspect a clean unified Incus image and write its public release manifest."""
2
+
3
+ import argparse
4
+ import hashlib
5
+ import json
6
+ import os
7
+ from pathlib import Path, PurePosixPath
8
+ import re
9
+ import tarfile
10
+
11
+
12
+ def credential_path(path):
13
+ """Reject credential locations even when a tar entry is a link or special file."""
14
+ return (
15
+ ".ssh" in path.parts
16
+ or (path.parent.name == "ssh" and path.name.startswith("ssh_host_"))
17
+ or path.name in {".netrc", "incus.json"}
18
+ or (path.name == ".npmrc" and path.parts[:2] in {("rootfs", "root"), ("rootfs", "home")})
19
+ )
20
+
21
+
22
+ def verify(archive, architecture, fingerprint):
23
+ if archive.stat().st_size >= 2 * 1024**3:
24
+ raise ValueError("Image exceeds GitHub's per-release-asset limit")
25
+ with archive.open("rb") as exported:
26
+ digest = hashlib.file_digest(exported, "sha256").hexdigest()
27
+ if digest != fingerprint:
28
+ raise ValueError("Export SHA256 does not match its immutable Incus fingerprint")
29
+ wanted = {
30
+ "rootfs/opt/raft/packages.tsv": f"packages-{architecture}.tsv",
31
+ "rootfs/opt/raft/npm-packages.json": f"npm-packages-{architecture}.json",
32
+ "metadata.yaml": None,
33
+ "rootfs/etc/machine-id": None,
34
+ }
35
+ found = set()
36
+ with tarfile.open(archive, "r|gz") as image:
37
+ for member in image:
38
+ path = PurePosixPath(member.name)
39
+ if path.is_absolute() or ".." in path.parts:
40
+ raise ValueError("Unsafe image member: " + member.name)
41
+ name = str(path)
42
+ if not member.isdir() and (
43
+ credential_path(path)
44
+ or path.parts[:2] == ("rootfs", "workspace")
45
+ or (
46
+ (member.issym() or member.islnk())
47
+ and credential_path(PurePosixPath(member.linkname))
48
+ )
49
+ ):
50
+ raise ValueError("Operator content or credential entry in template: " + name)
51
+ if not member.isfile():
52
+ continue
53
+ # npm itself ships a package-local .npmrc. Check all npm configs
54
+ # for credentials rather than treating every package config as a secret.
55
+ if path.name in {".npmrc", "npmrc"}:
56
+ content = image.extractfile(member).read().decode(errors="replace")
57
+ if re.search(
58
+ r"(?mi)^[^#;\n]*(?:_authToken|_auth|_password|password)\s*=\s*\S+", content
59
+ ):
60
+ raise ValueError("Credential setting in npm config: " + name)
61
+ if name in wanted:
62
+ content = image.extractfile(member).read()
63
+ found.add(name)
64
+ if name.endswith("machine-id") and content.strip():
65
+ raise ValueError("Template has a populated machine ID")
66
+ if wanted[name]:
67
+ (archive.parent / wanted[name]).write_bytes(content)
68
+ if found != set(wanted):
69
+ raise ValueError("Missing template resources: " + str(set(wanted) - found))
70
+ manifest = {
71
+ "architecture": architecture,
72
+ "filename": archive.name,
73
+ "sizeBytes": archive.stat().st_size,
74
+ "sha256": digest,
75
+ "incusFingerprint": fingerprint,
76
+ "sourceCommit": os.environ["GITHUB_SHA"],
77
+ "buildRun": f"https://github.com/{os.environ['GITHUB_REPOSITORY']}/actions/runs/{os.environ['GITHUB_RUN_ID']}",
78
+ "base": "Debian 13 default system container",
79
+ "isolation": "Unprivileged system container; shares the host kernel",
80
+ "checks": ["extended lifecycle", "31 developer tools", "same-host full-image recovery"],
81
+ "licensing": "Raft is MIT; bundled third-party packages retain their licenses in /usr/share/doc and upstream distributions",
82
+ }
83
+ (archive.parent / f"manifest-{architecture}.json").write_text(
84
+ json.dumps(manifest, indent=2) + "\n"
85
+ )
86
+ print("Clean image verified: " + archive.name)
87
+
88
+
89
+ if __name__ == "__main__":
90
+ parser = argparse.ArgumentParser(description=__doc__)
91
+ parser.add_argument("archive", type=Path)
92
+ parser.add_argument("--architecture", choices=["arm64", "amd64"], required=True)
93
+ parser.add_argument("--fingerprint", required=True)
94
+ args = parser.parse_args()
95
+ verify(args.archive, args.architecture, args.fingerprint)
@@ -0,0 +1,110 @@
1
+ """Check capacity boundaries and optionally real disposable box transitions."""
2
+
3
+ import argparse
4
+ import json
5
+ from pathlib import Path
6
+ import subprocess
7
+ import sys
8
+
9
+ ROOT = Path(__file__).resolve().parent.parent
10
+ sys.path.insert(0, str(ROOT))
11
+ from raft import capacity # noqa: E402
12
+
13
+ GIB = 1024**3
14
+ CLI = [sys.executable, str(ROOT / "raft.py")]
15
+
16
+
17
+ def policy_checks():
18
+ # These are allocation fixtures, not mocked host services. Expected counts
19
+ # describe the documented policy independently of its arithmetic.
20
+ host = {
21
+ "cpus": 8,
22
+ "memoryTotalBytes": 16 * GIB,
23
+ "memoryAvailableBytes": 14 * GIB,
24
+ "hostDiskFreeBytes": 40 * GIB,
25
+ "poolTotalBytes": 60 * GIB,
26
+ "poolFreeBytes": 30 * GIB,
27
+ }
28
+ running = {"status": "Running", "expanded_config": {"limits.cpu": "2", "limits.memory": "4GiB"}}
29
+ stopped = {"status": "Stopped"}
30
+ report = capacity(host, [running, stopped])
31
+ assert report["savedBoxes"] == 2 and report["activeBoxes"] == 1
32
+ assert report["allocated"] == {"cpus": 2, "memoryBytes": 4 * GIB}
33
+ size = next(r for r in report["recommendations"] if r["cpu"] == 2 and r["memoryGiB"] == 4)
34
+ assert (size["totalRunning"], size["additionalRunning"], size["newBoxes"]) == (3, 2, 2)
35
+ full = capacity(host, [stopped] * 4)
36
+ assert full["savedSlots"] == 0
37
+ assert all(r["newBoxes"] == 0 for r in full["recommendations"])
38
+ assert any(r["additionalRunning"] > 0 for r in full["recommendations"])
39
+ frozen = capacity(host, [{**running, "status": "Frozen"}])
40
+ assert frozen["allocated"] == report["allocated"]
41
+ low = capacity({**host, "memoryAvailableBytes": GIB, "poolFreeBytes": GIB}, [])
42
+ assert all(r["additionalRunning"] == 0 for r in low["recommendations"])
43
+ assert len(low["warnings"]) == 2
44
+ assert all(r["totalRunning"] == 0 for r in capacity({**host, "cpus": 1}, [])["recommendations"])
45
+ assert all(
46
+ r["totalRunning"] == 0
47
+ for r in capacity({**host, "memoryTotalBytes": GIB}, [])["recommendations"]
48
+ )
49
+ large = capacity({**host, "cpus": 64, "memoryTotalBytes": 128 * GIB}, [])
50
+ assert large["reserve"]["memoryBytes"] == 128 * GIB // 10
51
+ assert all(r["totalRunning"] == 4 for r in large["recommendations"])
52
+ over = capacity({**host, "cpus": 2}, [running])
53
+ assert over["warnings"] and all(r["additionalRunning"] == 0 for r in over["recommendations"])
54
+ for cpu, memory, expected in [(2, 8, 1), (4, 8, 3), (4, 16, 3), (8, 32, 4)]:
55
+ example = capacity({**host, "cpus": cpu, "memoryTotalBytes": memory * GIB}, [])
56
+ default = next(
57
+ r for r in example["recommendations"] if r["cpu"] == 1 and r["memoryGiB"] == 2
58
+ )
59
+ assert default["totalRunning"] == expected
60
+ print(
61
+ "Capacity policy: mixed sizes, stopped/frozen boxes, low RAM/disk, reserves and saved cap passed",
62
+ flush=True,
63
+ )
64
+
65
+
66
+ def command(*args):
67
+ return subprocess.check_output([*CLI, *args], text=True).strip()
68
+
69
+
70
+ def live_checks(location):
71
+ def limits():
72
+ return json.loads(command("limits", "--location", location, "--json"))
73
+
74
+ baseline = limits()
75
+ if baseline["savedSlots"] == 0:
76
+ raise RuntimeError("Capacity E2E needs one free saved-box slot")
77
+ box = command("new", "--location", location, "--cpu", "1", "--memory", "1GiB", "--ttl", "600")
78
+ try:
79
+ active = limits()
80
+ assert active["savedBoxes"] == baseline["savedBoxes"] + 1
81
+ assert active["activeBoxes"] == baseline["activeBoxes"] + 1
82
+ assert active["allocated"]["cpus"] == baseline["allocated"]["cpus"] + 1
83
+ assert active["allocated"]["memoryBytes"] == baseline["allocated"]["memoryBytes"] + GIB
84
+ assert active["savedSlots"] == baseline["savedSlots"] - 1
85
+ command("stop", box)
86
+ stopped = limits()
87
+ assert stopped["savedBoxes"] == active["savedBoxes"]
88
+ assert stopped["activeBoxes"] == baseline["activeBoxes"]
89
+ assert stopped["allocated"] == baseline["allocated"]
90
+ command("resume", box, "--ttl", "600")
91
+ resumed = limits()
92
+ assert resumed["activeBoxes"] == active["activeBoxes"]
93
+ assert resumed["allocated"] == active["allocated"]
94
+ assert "CPU RAM GiB" in command("limits", "--location", location)
95
+ finally:
96
+ command("destroy", box)
97
+ final = limits()
98
+ assert final["savedBoxes"] == baseline["savedBoxes"]
99
+ assert final["activeBoxes"] == baseline["activeBoxes"]
100
+ assert final["allocated"] == baseline["allocated"]
101
+ print(f"{location}: real capacity create/stop/resume/destroy reporting passed", flush=True)
102
+
103
+
104
+ if __name__ == "__main__":
105
+ parser = argparse.ArgumentParser(description=__doc__)
106
+ parser.add_argument("--location", help="Also test a real host using one disposable box")
107
+ args = parser.parse_args()
108
+ policy_checks()
109
+ if args.location:
110
+ live_checks(args.location)