feat: initialize XAS test console with native execution
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
"""Trusted original-environment recipes; user input never supplies executable code."""
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shlex
|
||||
import socket
|
||||
from urllib.parse import urlsplit
|
||||
from .catalog import script_path, safe_path
|
||||
|
||||
MAX_TIMEOUT = 72 * 3600
|
||||
PLACEHOLDER = re.compile(r"\{([a-zA-Z_][a-zA-Z0-9_]*)\}")
|
||||
|
||||
|
||||
def profile_for(settings, script):
|
||||
if script["kind"] != "external":
|
||||
return None
|
||||
source = next((s for s in settings.sources if s["id"] == script["source_group"]), None)
|
||||
if not source:
|
||||
return None
|
||||
expected = Path(script["source_path"])
|
||||
for profile in settings.native_catalog.get("profiles", []):
|
||||
if profile["source_group"] == source["id"] and Path(source["path"]) / profile["entry"] == expected:
|
||||
return profile
|
||||
return None
|
||||
|
||||
|
||||
def validate_options(profile, options):
|
||||
if not isinstance(options, dict):
|
||||
raise ValueError("原环境参数必须是对象")
|
||||
fields = {f["key"]: f for f in profile.get("parameters", [])}
|
||||
if set(options) - set(fields):
|
||||
raise ValueError("包含启动方案未登记的参数")
|
||||
result = {}
|
||||
for key, field in fields.items():
|
||||
value = options.get(key, field.get("default"))
|
||||
label = field.get("label", key)
|
||||
if value is None or value == "":
|
||||
if field.get("required", True):
|
||||
raise ValueError(f"请填写{label}")
|
||||
result[key] = ""
|
||||
continue
|
||||
kind = field["type"]
|
||||
if kind in {"integer", "number"}:
|
||||
valid = type(value) is int if kind == "integer" else type(value) in (int, float)
|
||||
if not valid or not math.isfinite(value) or value < field.get("min", 0) or value > field.get("max", 10000000):
|
||||
raise ValueError(f"{label}超出允许范围或类型不正确")
|
||||
elif kind == "select":
|
||||
if value not in [o["value"] for o in field["options"]]:
|
||||
raise ValueError(f"{label}必须从登记选项中选择")
|
||||
elif kind == "text":
|
||||
if not isinstance(value, str) or len(value) > 1024 or re.search(r"[\x00-\x1f;$`|&<>\\]", value):
|
||||
raise ValueError(f"{label}格式不正确")
|
||||
if "pattern" in field and not re.fullmatch(field["pattern"], value):
|
||||
raise ValueError(f"{label}格式不正确")
|
||||
else:
|
||||
raise ValueError("启动方案包含未知参数类型")
|
||||
result[key] = value
|
||||
return result
|
||||
|
||||
|
||||
def resolve_target(target):
|
||||
target = {**target, "environment": dict(target.get("environment", {}))}
|
||||
path = target.get("config_file")
|
||||
if not path:
|
||||
return target
|
||||
values = {}
|
||||
for line in Path(path).read_text().splitlines():
|
||||
match = re.match(r"^(PORT|SERVED|CT_OFF|CT_ON)=(.*)$", line.strip())
|
||||
if not match:continue
|
||||
parts = shlex.split(match[2], comments=True)
|
||||
if len(parts) != 1 or "$" in parts[0] or "`" in parts[0]:
|
||||
raise ValueError("原配置的模型/端口包含动态表达式,需管理员核对")
|
||||
values[match[1]] = parts[0]
|
||||
if "PORT" in values:
|
||||
port = int(values["PORT"])
|
||||
if not 1 <= port <= 65535:raise ValueError("原模型端口不合法")
|
||||
target["url"] = "http://127.0.0.1:" + str(port)
|
||||
if "SERVED" in values:target["model"] = values["SERVED"]
|
||||
for key in ("CT_OFF", "CT_ON"):
|
||||
if key in values:target["environment"][key] = values[key]
|
||||
return target
|
||||
|
||||
|
||||
def public_execution(settings, script):
|
||||
profile = profile_for(settings, script)
|
||||
if profile is None:
|
||||
return {"mode": "container", "interactive": False, "max_timeout_seconds": 7200}
|
||||
return {"mode": "native", "profile_id": profile["id"], "interpreter": profile["interpreter"],
|
||||
"user": profile["user"], "cwd": profile["cwd"], "description": "使用服务器原有环境和原始入口,不迁移依赖或修改源文件。",
|
||||
"impact": profile["impact"], "parameters": profile.get("parameters", []),
|
||||
"interactive": profile.get("interactive", False), "max_timeout_seconds": MAX_TIMEOUT,
|
||||
"notes": profile.get("notes", []), "verification": "启动方案已接入,未完成本轮 GPU/长稳业务验收"}
|
||||
|
||||
|
||||
def validate_config(settings, script, values):
|
||||
profile = profile_for(settings, script)
|
||||
if not profile:
|
||||
raise ValueError("未登记原环境方案")
|
||||
config = dict(values)
|
||||
config["native_options"] = validate_options(profile, config.get("native_options", {}))
|
||||
timeout = config.get("timeout_seconds", profile.get("timeout_seconds", 3600))
|
||||
if type(timeout) is not int or not 5 <= timeout <= MAX_TIMEOUT:
|
||||
raise ValueError("原环境时限须为 5 秒至 72 小时")
|
||||
config.update(timeout_seconds=timeout, cwd=profile["cwd"], args=[], output_paths=[], profile="native")
|
||||
return config
|
||||
|
||||
|
||||
def build_plan(settings, script, run_id=None, check_host=True):
|
||||
profile = profile_for(settings, script)
|
||||
if not profile:
|
||||
raise ValueError("该入口未登记原环境方案")
|
||||
source = script_path(settings, script)
|
||||
source_hash = hashlib.sha256(source.read_bytes()).hexdigest()
|
||||
if source_hash != script["sha256"]:
|
||||
raise ValueError("原入口已变更,请刷新来源并重新确认后运行")
|
||||
options = validate_options(profile, script["config"].get("native_options", {}))
|
||||
target_id = options.get("target")
|
||||
target = next((t for t in settings.native_catalog.get("targets", []) if t["id"] == target_id), {})
|
||||
if target_id and not target:
|
||||
raise ValueError("目标服务未登记")
|
||||
if target:
|
||||
target = resolve_target(target)
|
||||
if run_id and not re.fullmatch(r"[a-f0-9]{32}", run_id):
|
||||
raise ValueError("运行编号格式错误")
|
||||
identity = run_id or "{run_id}"
|
||||
directory = settings.runs / identity
|
||||
variables = {**options, "run_id": identity, "run_label": "console-" + identity,
|
||||
"run_dir": str(directory), "output_file": str(Path(profile["cwd"]) / "logs" / f"console-{identity}-{profile['id']}.json"),
|
||||
"target_url": target.get("url", ""), "target_model": target.get("model", ""),
|
||||
"target_port": urlsplit(target.get("url", "")).port or "", "target_config": target.get("config_name", "")}
|
||||
def render(value):
|
||||
def replace(match):
|
||||
if match[1] not in variables:
|
||||
raise ValueError("原环境方案包含未定义的占位参数")
|
||||
return str(variables[match[1]])
|
||||
return PLACEHOLDER.sub(replace, str(value))
|
||||
argv = [profile["interpreter"], str(source), *[render(a) for a in profile.get("arguments", [])]]
|
||||
environment = {**target.get("environment", {}), **{k: render(v) for k, v in profile.get("environment", {}).items()}}
|
||||
credential_files = list(dict.fromkeys(profile.get("credential_files", []) + target.get("credential_files", [])))
|
||||
setup_files = [render(p) for p in profile.get("env_setup_files", [])]
|
||||
required_files = [render(p) for p in profile.get("required_files", [])]
|
||||
if target.get("config_file"):
|
||||
required_files.append(target["config_file"])
|
||||
timeout = script["config"].get("timeout_seconds", profile.get("timeout_seconds", 3600))
|
||||
if type(timeout) is not int or not 5 <= timeout <= MAX_TIMEOUT:
|
||||
raise ValueError("原环境时限不合法")
|
||||
checks = []
|
||||
def check(name, ok, message):
|
||||
checks.append({"name": name, "ok": bool(ok), "message": message})
|
||||
check("启动方案", profile.get("available", True), profile.get("unavailable_reason", "已登记原环境入口"))
|
||||
if check_host:
|
||||
check("原工作目录", Path(profile["cwd"]).is_dir(), profile["cwd"])
|
||||
tools = list(dict.fromkeys([profile["interpreter"], "/usr/bin/systemd-run", *profile.get("required_tools", [])]))
|
||||
for tool in tools:
|
||||
check("工具", Path(tool).is_file() and os.access(tool, os.X_OK), tool)
|
||||
for path in required_files + credential_files + setup_files:
|
||||
check("原配置或素材", Path(path).is_file(), path)
|
||||
for value in profile.get("check_ports", []):
|
||||
port = int(render(value))
|
||||
if not 1 <= port <= 65535:
|
||||
raise ValueError("原服务端口不合法")
|
||||
try:
|
||||
with socket.create_connection(("127.0.0.1", port), timeout=.3):pass
|
||||
available = True
|
||||
except OSError:available = False
|
||||
check("目标服务端口", available, f"127.0.0.1:{port}" + (" 可连接" if available else " 未监听,请先准备原服务"))
|
||||
# Binds acknowledgment to saved configuration, source and recipe, not to a mutable browser flag.
|
||||
dependency_hashes = {}
|
||||
for name in required_files + setup_files:
|
||||
path = Path(name)
|
||||
if name not in credential_files and path.is_file() and path.stat().st_size <= 16 * 1024**2:
|
||||
dependency_hashes[name] = hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
fingerprint = {"profile": profile, "target": target, "options": options, "source": source_hash,
|
||||
"dependencies": dependency_hashes,
|
||||
"script_id": script["id"], "timeout_seconds": timeout}
|
||||
token = hashlib.sha256(json.dumps(fingerprint, sort_keys=True, ensure_ascii=False).encode()).hexdigest()
|
||||
return {"mode": "native", "profile_id": profile["id"], "plan_token": token,
|
||||
"ready": all(c["ok"] for c in checks), "checks": checks, "argv": argv, "command_preview": shlex.join(argv),
|
||||
"cwd": profile["cwd"], "user": profile["user"], "environment": environment,
|
||||
"credential_files": credential_files, "env_setup_files": setup_files,
|
||||
"impact": profile["impact"], "interactive": profile.get("interactive", False),
|
||||
"timeout_seconds": timeout, "source_sha256": source_hash, "dependency_hashes": dependency_hashes,
|
||||
"output_paths": [render(p) for p in profile.get("output_paths", [])],
|
||||
"notes": profile.get("notes", []) + ([f"目标服务:{target['url']};模型:{target['model']}(原配置已复核)"] if target else [])}
|
||||
|
||||
|
||||
def public_plan(plan):
|
||||
keys = ("mode", "profile_id", "plan_token", "ready", "checks", "command_preview", "cwd", "user",
|
||||
"impact", "interactive", "output_paths", "notes", "timeout_seconds", "source_sha256")
|
||||
return {key: plan[key] for key in keys}
|
||||
Reference in New Issue
Block a user