guanghu-ice-heart/server-tools/hololake-code-channel/jd-candidate/hlcc-bootstrap.py

532 lines
19 KiB
Python

#!/usr/bin/env python3
"""Verify and start the isolated JD HoloLake Code Channel candidate."""
from __future__ import annotations
import hashlib
import http.server
import json
import os
import pathlib
import re
import shutil
import sqlite3
import subprocess
import tempfile
import threading
import time
import urllib.error
import urllib.parse
import urllib.request
VERSION = "16.0.1"
UPSTREAM_FINGERPRINT = "EB114F5E6C0DC2BCDD183550A4B61A2DC5923710"
STATE_ROOT = pathlib.Path("/var/lib/guanghu/personas/guanghu/hlcc-v16.0.1")
LEGACY_DB = STATE_ROOT / "data" / "owner-identity-source.db"
OWNER_NAME = "bingshuo"
CHANNEL_REPOSITORY = "guanghu-ice-heart"
LEGACY_REPOSITORY_URL = "https://guanghulab.com/fifth-domain/bingshuo/fifth-domain.git"
SEED_COMMIT_NUMBER = "HLCC-ICE-000001"
SEED_CONTRIBUTION_NUMBER = "ZY-CONTRIB-20260723-001"
RELAY_ROOT = "https://guanghubingshuo.com/hlcc-offline/16.0.1"
BINARY_NAME = f"forgejo-{VERSION}-linux-amd64"
EXPECTED = {
BINARY_NAME: "7a4c568136650c10498a9d3d62c7fd630a0cf09c166293ebd78708248f6398fc",
f"{BINARY_NAME}.asc": "1c0ca36df3adb0a7692b6bdc84d7886001ca0c6d0408e67c9d232d2f33cecc71",
"forgejo-release-key.asc": "6fae8894c671ce2397cb35fe40c324f73deade6b4cb3cd6cedd1d2b248e0e3ea",
"forgejo-upstream-all.bundle": "c33bd074d9b2896259e86ebe03ad31ccdd8ff71897beed4320081fa03b15381f",
"guanghu-code-channel.bundle": "fc53740259d108128e69f5a809cec438ecf3158175617574ba55b8612c5eaa6c",
"MANIFEST.sha256": "d564c3b600d4b7a199d8a04ce505ceabf81993ca74fa440805601d55e550f185",
}
STATUS = {
"ok": True,
"mode": "bootstrap",
"version": VERSION,
"code": "HLCC-JD-CANDIDATE-01",
"ready": False,
"stage": "starting",
"package_profile": "full-offline-v16.0.1",
}
STATUS_LOCK = threading.Lock()
def set_status(**values: object) -> None:
with STATUS_LOCK:
STATUS.update(values)
class HealthHandler(http.server.BaseHTTPRequestHandler):
def do_GET(self) -> None: # noqa: N802 - BaseHTTPRequestHandler API
if self.path != "/health":
self.send_error(404)
return
with STATUS_LOCK:
payload = json.dumps(STATUS, ensure_ascii=False).encode("utf-8")
self.send_response(200)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Cache-Control", "no-store")
self.send_header("Content-Length", str(len(payload)))
self.end_headers()
self.wfile.write(payload)
def log_message(self, _format: str, *_args: object) -> None:
return
def sha256(path: pathlib.Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as handle:
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def download_verified(name: str) -> pathlib.Path:
destination = STATE_ROOT / "release" / name
if destination.is_file() and sha256(destination) == EXPECTED[name]:
return destination
destination.parent.mkdir(parents=True, exist_ok=True)
temporary = destination.with_suffix(destination.suffix + ".partial")
temporary.unlink(missing_ok=True)
request = urllib.request.Request(
f"{RELAY_ROOT}/{name}",
headers={"User-Agent": "HoloLake-Code-Channel/16.0.1"},
)
with urllib.request.urlopen(request, timeout=45) as response, temporary.open("wb") as output:
shutil.copyfileobj(response, output, length=1024 * 1024)
if sha256(temporary) != EXPECTED[name]:
temporary.unlink(missing_ok=True)
raise RuntimeError(f"sha256 mismatch: {name}")
temporary.replace(destination)
return destination
def verify_release(files: dict[str, pathlib.Path]) -> None:
manifest = files["MANIFEST.sha256"].read_text(encoding="utf-8")
for name in (
BINARY_NAME,
f"{BINARY_NAME}.asc",
"forgejo-release-key.asc",
"forgejo-upstream-all.bundle",
"guanghu-code-channel.bundle",
):
expected_line = f"{EXPECTED[name]} {name}"
if expected_line not in manifest.splitlines():
raise RuntimeError(f"manifest entry mismatch: {name}")
gpg_home = STATE_ROOT / "gpg"
gpg_home.mkdir(parents=True, exist_ok=True)
gpg_home.chmod(0o700)
environment = {**os.environ, "GNUPGHOME": str(gpg_home)}
subprocess.run(
["gpg", "--batch", "--import", str(files["forgejo-release-key.asc"])],
check=True,
env=environment,
stdout=subprocess.DEVNULL,
)
fingerprint = subprocess.run(
["gpg", "--batch", "--with-colons", "--fingerprint", UPSTREAM_FINGERPRINT],
check=True,
env=environment,
text=True,
capture_output=True,
).stdout
if f"fpr:::::::::{UPSTREAM_FINGERPRINT}:" not in fingerprint:
raise RuntimeError("release key fingerprint mismatch")
subprocess.run(
[
"gpg",
"--batch",
"--verify",
str(files[f"{BINARY_NAME}.asc"]),
str(files[BINARY_NAME]),
],
check=True,
env=environment,
stdout=subprocess.DEVNULL,
)
def wait_for_candidate(process: subprocess.Popen[bytes]) -> None:
url = "http://127.0.0.1:3340/api/healthz"
for _attempt in range(90):
if process.poll() is not None:
raise RuntimeError(f"candidate exited with code {process.returncode}")
try:
with urllib.request.urlopen(url, timeout=2) as response:
payload = json.load(response)
if payload.get("status") == "pass":
return
except Exception: # Candidate is still starting.
pass
time.sleep(1)
raise RuntimeError("candidate readiness timeout")
def immutable_source_commit(script_path: pathlib.Path | None = None) -> str:
source = (script_path or pathlib.Path(__file__)).resolve()
matches = [part for part in source.parts if re.fullmatch(r"[0-9a-f]{40}", part)]
if len(matches) != 1:
raise RuntimeError("immutable release commit unavailable")
return matches[0]
def owner_identity_columns(connection: sqlite3.Connection) -> list[str]:
return [row[1] for row in connection.execute("pragma table_info(user)")]
def migrate_owner_identity(
legacy_db: pathlib.Path = LEGACY_DB,
channel_db: pathlib.Path | None = None,
receipt_path: pathlib.Path | None = None,
) -> str:
target = channel_db or STATE_ROOT / "data" / "hlcc.db"
receipt = receipt_path or STATE_ROOT / "data" / "owner-migration-receipt.json"
if not legacy_db.is_file() or not target.is_file():
raise RuntimeError("owner migration database unavailable")
legacy = sqlite3.connect(f"file:{legacy_db}?mode=ro", uri=True, timeout=15)
channel = sqlite3.connect(target, timeout=15)
try:
existing = channel.execute(
"select is_active, is_admin from user where lower_name = ?",
(OWNER_NAME,),
).fetchall()
if existing:
if len(existing) != 1 or existing[0] != (1, 1):
raise RuntimeError("channel owner identity mismatch")
return "already-present"
source_rows = legacy.execute(
"select * from user where lower_name = ?",
(OWNER_NAME,),
).fetchall()
if len(source_rows) != 1:
raise RuntimeError("legacy owner identity mismatch")
old_columns = owner_identity_columns(legacy)
new_info = list(channel.execute("pragma table_info(user)"))
new_columns = {row[1] for row in new_info}
missing = [
row[1]
for row in new_info
if row[3] and row[4] is None and row[1] != "id" and row[1] not in old_columns
]
if missing:
raise RuntimeError("channel owner schema has unsupported required columns")
copied_columns = [name for name in old_columns if name in new_columns and name != "id"]
values = dict(zip(old_columns, source_rows[0]))
for counter in ("num_repos", "num_stars", "num_followers", "num_following"):
if counter in values:
values[counter] = 0
if "use_custom_avatar" in values:
values["use_custom_avatar"] = 0
if "prohibit_login" in values:
values["prohibit_login"] = 0
backup_dir = target.parent / "backups"
backup_dir.mkdir(parents=True, exist_ok=True)
backup_path = backup_dir / "hlcc-before-owner-migration.db"
backup = sqlite3.connect(backup_path)
try:
channel.backup(backup)
finally:
backup.close()
backup_path.chmod(0o600)
placeholders = ",".join("?" for _name in copied_columns)
column_sql = ",".join(f'"{name}"' for name in copied_columns)
with channel:
channel.execute(
f"insert into user ({column_sql}) values ({placeholders})",
[values[name] for name in copied_columns],
)
verified = channel.execute(
"select id, is_active, is_admin from user where lower_name = ?",
(OWNER_NAME,),
).fetchall()
if len(verified) != 1 or verified[0][1:] != (1, 1):
raise RuntimeError("channel owner migration verification failed")
receipt.write_text(
json.dumps(
{
"schema": "guanghu.hlcc-owner-migration/v1",
"owner": OWNER_NAME,
"identity_only": True,
"password_hash_preserved": True,
"access_tokens_migrated": False,
"repositories_migrated": False,
"result": "VERIFIED",
},
ensure_ascii=False,
indent=2,
)
+ "\n",
encoding="utf-8",
)
receipt.chmod(0o600)
return "migrated"
finally:
channel.close()
legacy.close()
def api_json(
method: str,
path: str,
token: str = "",
body: dict[str, object] | None = None,
) -> tuple[int, dict[str, object]]:
headers = {"Accept": "application/json"}
data = None
if token:
headers["Authorization"] = f"token {token}"
if body is not None:
headers["Content-Type"] = "application/json"
data = json.dumps(body).encode("utf-8")
request = urllib.request.Request(
f"http://127.0.0.1:3340{path}",
data=data,
headers=headers,
method=method,
)
try:
with urllib.request.urlopen(request, timeout=15) as response:
raw = response.read()
return response.status, json.loads(raw or b"{}")
except urllib.error.HTTPError as error:
raw = error.read()
try:
payload = json.loads(raw or b"{}")
except json.JSONDecodeError:
payload = {}
return error.code, payload
def delete_bootstrap_token(channel_db: pathlib.Path, token_name: str) -> None:
connection = sqlite3.connect(channel_db, timeout=15)
try:
with connection:
owner = connection.execute(
"select id from user where lower_name = ?",
(OWNER_NAME,),
).fetchone()
if owner:
connection.execute(
"delete from access_token where uid = ? and name = ?",
(owner[0], token_name),
)
finally:
connection.close()
def generate_bootstrap_token(binary: pathlib.Path, token_name: str) -> str:
channel_db = STATE_ROOT / "data" / "hlcc.db"
delete_bootstrap_token(channel_db, token_name)
result = subprocess.run(
[
str(binary),
"admin",
"user",
"generate-access-token",
"--username",
OWNER_NAME,
"--token-name",
token_name,
"--scopes",
"write:repository",
"--raw",
"--config",
str(STATE_ROOT / "config" / "app.ini"),
"--work-path",
str(STATE_ROOT / "data"),
],
check=True,
capture_output=True,
text=True,
)
token = result.stdout.strip().splitlines()[-1]
if not re.fullmatch(r"[A-Za-z0-9_-]{32,160}", token):
raise RuntimeError("bootstrap access token format invalid")
return token
def seed_fifth_domain_channel(binary: pathlib.Path) -> str:
status, repository = api_json("GET", f"/api/v1/repos/{OWNER_NAME}/{CHANNEL_REPOSITORY}")
if status == 200:
if repository.get("private") is not False:
raise RuntimeError("existing channel repository is not public")
if repository.get("empty") is False and repository.get("default_branch") == "main":
return "already-present"
if repository.get("empty") is not True:
raise RuntimeError("existing channel repository state invalid")
elif status != 404:
raise RuntimeError("channel repository lookup failed")
token_name = "hlcc-fifth-domain-bootstrap"
token = generate_bootstrap_token(binary, token_name)
channel_db = STATE_ROOT / "data" / "hlcc.db"
try:
if status == 404:
created_status, _created = api_json(
"POST",
"/api/v1/user/repos",
token,
{
"name": CHANNEL_REPOSITORY,
"description": "光湖代码频道 · 冰朔第五域个人子频道 · 2026-07-23 新起点",
"private": False,
"auto_init": False,
"default_branch": "main",
},
)
if created_status != 201:
raise RuntimeError("channel repository creation failed")
source_commit = immutable_source_commit()
with tempfile.TemporaryDirectory(prefix="hlcc-seed-") as temporary:
root = pathlib.Path(temporary)
snapshot = root / "snapshot"
subprocess.run(
["git", "clone", "--depth=1", "--branch", "main", LEGACY_REPOSITORY_URL, str(snapshot)],
check=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE,
)
actual_commit = subprocess.run(
["git", "-C", str(snapshot), "rev-parse", "HEAD"],
check=True,
capture_output=True,
text=True,
).stdout.strip()
if actual_commit != source_commit:
raise RuntimeError("legacy snapshot commit mismatch")
shutil.rmtree(snapshot / ".git")
subprocess.run(["git", "-C", str(snapshot), "init", "-b", "main"], check=True, capture_output=True)
subprocess.run(
["git", "-C", str(snapshot), "config", "user.name", "光湖代码频道 · 铸渊"],
check=True,
)
subprocess.run(
["git", "-C", str(snapshot), "config", "user.email", "hlcc@guanghulab.invalid"],
check=True,
)
subprocess.run(["git", "-C", str(snapshot), "add", "-A"], check=True)
subprocess.run(
[
"git",
"-C",
str(snapshot),
"commit",
"-m",
(
f"[{SEED_COMMIT_NUMBER}][{SEED_CONTRIBUTION_NUMBER}] "
"feat: 以来光者贡献链启用冰朔第五域个人子频道"
),
],
check=True,
capture_output=True,
)
credential = root / "credentials"
encoded = urllib.parse.quote(token, safe="")
credential.write_text(
f"http://{OWNER_NAME}:{encoded}@127.0.0.1:3340\n",
encoding="utf-8",
)
credential.chmod(0o600)
subprocess.run(
[
"git",
"-C",
str(snapshot),
"-c",
f"credential.helper=store --file {credential}",
"push",
"http://127.0.0.1:3340/bingshuo/guanghu-ice-heart.git",
"main:main",
],
check=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE,
)
credential.unlink(missing_ok=True)
verified_status, verified = api_json(
"GET",
f"/api/v1/repos/{OWNER_NAME}/{CHANNEL_REPOSITORY}",
)
if (
verified_status != 200
or verified.get("private") is not False
or verified.get("empty") is not False
or verified.get("default_branch") != "main"
):
raise RuntimeError("channel repository verification failed")
return "seeded"
finally:
delete_bootstrap_token(channel_db, token_name)
def bootstrap() -> None:
process: subprocess.Popen[bytes] | None = None
try:
for directory in ("config", "data", "logs", "release", "tmp"):
(STATE_ROOT / directory).mkdir(parents=True, exist_ok=True)
set_status(stage="downloading")
files = {name: download_verified(name) for name in EXPECTED}
set_status(stage="verifying")
verify_release(files)
binary = files[BINARY_NAME]
binary.chmod(0o755)
config_source = pathlib.Path(__file__).with_name("app.ini")
config_target = STATE_ROOT / "config" / "app.ini"
shutil.copyfile(config_source, config_target)
config_target.chmod(0o600)
custom_source = pathlib.Path(__file__).with_name("custom")
custom_target = STATE_ROOT / "data" / "custom"
if custom_source.is_dir():
shutil.copytree(custom_source, custom_target, dirs_exist_ok=True)
set_status(stage="launching")
log_path = STATE_ROOT / "logs" / "hlcc.log"
log_handle = log_path.open("ab", buffering=0)
process = subprocess.Popen(
[
str(binary),
"web",
"--work-path",
str(STATE_ROOT / "data"),
"--config",
str(config_target),
],
stdout=log_handle,
stderr=subprocess.STDOUT,
)
wait_for_candidate(process)
set_status(stage="migrating-owner")
migrate_owner_identity()
set_status(stage="seeding-fifth-domain-channel")
seed_fifth_domain_channel(binary)
set_status(mode="isolated-candidate", ready=True, stage="ready")
return_code = process.wait()
raise RuntimeError(f"candidate stopped with code {return_code}")
except Exception as error:
if process and process.poll() is None:
process.terminate()
set_status(ok=False, ready=False, stage="failed", error=str(error)[:180])
def main() -> None:
thread = threading.Thread(target=bootstrap, name="hlcc-bootstrap", daemon=True)
thread.start()
server = http.server.ThreadingHTTPServer(("127.0.0.1", 3341), HealthHandler)
server.serve_forever()
if __name__ == "__main__":
main()