Миграция оркестратора на Debian 13: platform layer, preflight, status/diagnostics и structured logging
This commit is contained in:
@@ -1,6 +1,8 @@
|
|||||||
import { install } from "./commands/install";
|
import { install } from "./commands/install";
|
||||||
import { reconfigure } from "./commands/reconfigure";
|
import { reconfigure } from "./commands/reconfigure";
|
||||||
import { doctor } from "./commands/doctor";
|
import { doctor } from "./commands/doctor";
|
||||||
|
import { status } from "./commands/status";
|
||||||
|
import { diagnosticsCollect } from "./commands/diagnostics";
|
||||||
import type { InstallOptions, ReconfigureOptions } from "./types/context";
|
import type { InstallOptions, ReconfigureOptions } from "./types/context";
|
||||||
|
|
||||||
function usage(): never {
|
function usage(): never {
|
||||||
@@ -8,6 +10,8 @@ function usage(): never {
|
|||||||
console.error(" hy2xs-orchestrator install --package-dir <path> [--config <path>] [--skip-firewall] [--skip-start] [--non-interactive]");
|
console.error(" hy2xs-orchestrator install --package-dir <path> [--config <path>] [--skip-firewall] [--skip-start] [--non-interactive]");
|
||||||
console.error(" hy2xs-orchestrator reconfigure --package-dir <path> [--config <path>] [--dry-run|--apply] [--skip-firewall] [--skip-start]");
|
console.error(" hy2xs-orchestrator reconfigure --package-dir <path> [--config <path>] [--dry-run|--apply] [--skip-firewall] [--skip-start]");
|
||||||
console.error(" hy2xs-orchestrator doctor --package-dir <path> [--config <path>] [--skip-firewall] [--skip-start]");
|
console.error(" hy2xs-orchestrator doctor --package-dir <path> [--config <path>] [--skip-firewall] [--skip-start]");
|
||||||
|
console.error(" hy2xs-orchestrator status --package-dir <path> [--config <path>] [--skip-firewall] [--skip-start]");
|
||||||
|
console.error(" hy2xs-orchestrator diagnostics collect --package-dir <path> [--config <path>] [--skip-firewall] [--skip-start]");
|
||||||
process.exit(2);
|
process.exit(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,6 +122,10 @@ function parseReconfigureOptions(args: string[]): ReconfigureOptions {
|
|||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseCommonOptions(args: string[]): InstallOptions {
|
||||||
|
return parseInstallOptions(args);
|
||||||
|
}
|
||||||
|
|
||||||
async function main(): Promise<void> {
|
async function main(): Promise<void> {
|
||||||
const [command, ...args] = Bun.argv.slice(2);
|
const [command, ...args] = Bun.argv.slice(2);
|
||||||
if (command === "install") {
|
if (command === "install") {
|
||||||
@@ -133,6 +141,18 @@ async function main(): Promise<void> {
|
|||||||
await doctor(options);
|
await doctor(options);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (command === "status") {
|
||||||
|
await status(parseCommonOptions(args));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (command === "diagnostics") {
|
||||||
|
const [subcommand, ...rest] = args;
|
||||||
|
if (subcommand !== "collect") {
|
||||||
|
usage();
|
||||||
|
}
|
||||||
|
await diagnosticsCollect(parseCommonOptions(rest));
|
||||||
|
return;
|
||||||
|
}
|
||||||
usage();
|
usage();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import type { CommonOptions } from "../types/context";
|
||||||
|
import { info, setOperationContext } from "../lib/log";
|
||||||
|
import { run } from "../lib/process";
|
||||||
|
|
||||||
|
function shellEscapeSingleQuotes(value: string): string {
|
||||||
|
return value.replaceAll("'", "'\\''");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function diagnosticsCollect(_options: CommonOptions): Promise<void> {
|
||||||
|
setOperationContext(`diag-${Date.now().toString(36)}`);
|
||||||
|
|
||||||
|
const outDir = `/tmp/hy2xs-diagnostics-${Date.now()}`;
|
||||||
|
await run`mkdir -p ${outDir}`;
|
||||||
|
|
||||||
|
await run`sh -c ${`systemctl status hysteria-server > '${shellEscapeSingleQuotes(`${outDir}/systemd-hysteria.txt`)}' 2>&1 || true`}`;
|
||||||
|
await run`sh -c ${`systemctl status hy2xs-admin > '${shellEscapeSingleQuotes(`${outDir}/systemd-admin.txt`)}' 2>&1 || true`}`;
|
||||||
|
await run`sh -c ${`journalctl -u hysteria-server -n 300 --no-pager > '${shellEscapeSingleQuotes(`${outDir}/journal-hysteria.log`)}' 2>&1 || true`}`;
|
||||||
|
await run`sh -c ${`journalctl -u hy2xs-admin -n 300 --no-pager > '${shellEscapeSingleQuotes(`${outDir}/journal-admin.log`)}' 2>&1 || true`}`;
|
||||||
|
await run`sh -c ${`nft list ruleset > '${shellEscapeSingleQuotes(`${outDir}/nftables.ruleset`)}' 2>&1 || true`}`;
|
||||||
|
await run`sh -c ${`uname -a > '${shellEscapeSingleQuotes(`${outDir}/uname.txt`)}' 2>&1 || true`}`;
|
||||||
|
await run`sh -c ${`cat /etc/os-release > '${shellEscapeSingleQuotes(`${outDir}/os-release.txt`)}' 2>&1 || true`}`;
|
||||||
|
await run`sh -c ${`cp -a /etc/hysteria/post-install.env '${shellEscapeSingleQuotes(`${outDir}/post-install.env`)}' 2>/dev/null || true`}`;
|
||||||
|
await run`sh -c ${`cp -a /var/lib/hy2xs/install-state.json '${shellEscapeSingleQuotes(`${outDir}/install-state.json`)}' 2>/dev/null || true`}`;
|
||||||
|
|
||||||
|
info(`diagnostics bundle collected: ${outDir}`);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
import type { ReconfigureContext, ReconfigureOptions } from "../types/context";
|
import type { ReconfigureContext, ReconfigureOptions } from "../types/context";
|
||||||
import { readText } from "../lib/fs";
|
import { readText } from "../lib/fs";
|
||||||
import { step } from "../lib/log";
|
import { setOperationContext, step, stepDone } from "../lib/log";
|
||||||
import { parseRuntimeEnv } from "../config/env";
|
import { parseRuntimeEnv } from "../config/env";
|
||||||
import { preflight } from "../steps/preflight";
|
import { preflight } from "../steps/preflight";
|
||||||
import { smoke } from "../steps/smoke";
|
import { smoke } from "../steps/smoke";
|
||||||
import { readInstalledHysteriaVersion, readPackageValue } from "../lib/packageMeta";
|
import { readInstalledHysteriaVersion, readPackageValue } from "../lib/packageMeta";
|
||||||
|
|
||||||
export async function doctor(options: ReconfigureOptions): Promise<void> {
|
export async function doctor(options: ReconfigureOptions): Promise<void> {
|
||||||
|
setOperationContext(`doctor-${Date.now().toString(36)}`);
|
||||||
const configRaw = await readText(options.sourceConfigPath);
|
const configRaw = await readText(options.sourceConfigPath);
|
||||||
const config = parseRuntimeEnv(configRaw);
|
const config = parseRuntimeEnv(configRaw);
|
||||||
|
|
||||||
@@ -22,7 +23,9 @@ export async function doctor(options: ReconfigureOptions): Promise<void> {
|
|||||||
|
|
||||||
step("doctor preflight");
|
step("doctor preflight");
|
||||||
await preflight(context);
|
await preflight(context);
|
||||||
|
stepDone("doctor preflight");
|
||||||
step("doctor smoke");
|
step("doctor smoke");
|
||||||
await smoke(context);
|
await smoke(context);
|
||||||
|
stepDone("doctor smoke");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { InstallContext, InstallOptions } from "../types/context";
|
import type { InstallContext, InstallOptions } from "../types/context";
|
||||||
import { exists, readText, writeText, writeTextAtomic } from "../lib/fs";
|
import { exists, readText, writeText, writeTextAtomic } from "../lib/fs";
|
||||||
import { runVisible } from "../lib/process";
|
import { runVisible } from "../lib/process";
|
||||||
import { step } from "../lib/log";
|
import { setOperationContext, step, stepDone } from "../lib/log";
|
||||||
import { readPackageValue } from "../lib/packageMeta";
|
import { readPackageValue } from "../lib/packageMeta";
|
||||||
import { parseRuntimeEnv, renderRuntimeEnv } from "../config/env";
|
import { parseRuntimeEnv, renderRuntimeEnv } from "../config/env";
|
||||||
import { preflight } from "../steps/preflight";
|
import { preflight } from "../steps/preflight";
|
||||||
@@ -43,6 +43,7 @@ async function rollbackFailedInstall(context: InstallContext, state: { firewallT
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function install(options: InstallOptions): Promise<void> {
|
export async function install(options: InstallOptions): Promise<void> {
|
||||||
|
setOperationContext(`install-${Date.now().toString(36)}`);
|
||||||
const hasSourceConfig = options.sourceConfigPath ? await exists(options.sourceConfigPath) : false;
|
const hasSourceConfig = options.sourceConfigPath ? await exists(options.sourceConfigPath) : false;
|
||||||
if (options.sourceConfigPath && !hasSourceConfig) {
|
if (options.sourceConfigPath && !hasSourceConfig) {
|
||||||
throw new Error(`config source not found: ${options.sourceConfigPath}`);
|
throw new Error(`config source not found: ${options.sourceConfigPath}`);
|
||||||
@@ -75,10 +76,13 @@ export async function install(options: InstallOptions): Promise<void> {
|
|||||||
try {
|
try {
|
||||||
step("preflight");
|
step("preflight");
|
||||||
await preflight(context);
|
await preflight(context);
|
||||||
|
stepDone("preflight");
|
||||||
step("system dependencies");
|
step("system dependencies");
|
||||||
await installDeps(context);
|
await installDeps(context);
|
||||||
|
stepDone("system dependencies");
|
||||||
step("filesystem");
|
step("filesystem");
|
||||||
await prepareFilesystem(context);
|
await prepareFilesystem(context);
|
||||||
|
stepDone("filesystem");
|
||||||
step("write runtime env");
|
step("write runtime env");
|
||||||
await runVisible`mkdir -p /etc/hy2xs`;
|
await runVisible`mkdir -p /etc/hy2xs`;
|
||||||
await writeTextAtomic(options.runtimeConfigPath, renderRuntimeEnv(config), {
|
await writeTextAtomic(options.runtimeConfigPath, renderRuntimeEnv(config), {
|
||||||
@@ -86,27 +90,38 @@ export async function install(options: InstallOptions): Promise<void> {
|
|||||||
owner: "root",
|
owner: "root",
|
||||||
group: "root"
|
group: "root"
|
||||||
});
|
});
|
||||||
|
stepDone("write runtime env");
|
||||||
step("bundled UI");
|
step("bundled UI");
|
||||||
await deployUi(context);
|
await deployUi(context);
|
||||||
|
stepDone("bundled UI");
|
||||||
step("Hysteria2 upstream install");
|
step("Hysteria2 upstream install");
|
||||||
await installHysteria(context);
|
await installHysteria(context);
|
||||||
|
stepDone("Hysteria2 upstream install");
|
||||||
step("config generation");
|
step("config generation");
|
||||||
await generateConfig(context);
|
await generateConfig(context);
|
||||||
|
stepDone("config generation");
|
||||||
step("systemd units");
|
step("systemd units");
|
||||||
await deploySystemd(context);
|
await deploySystemd(context);
|
||||||
|
stepDone("systemd units");
|
||||||
step("firewall");
|
step("firewall");
|
||||||
state.firewallTouched = true;
|
state.firewallTouched = true;
|
||||||
await applyFirewall(context);
|
await applyFirewall(context);
|
||||||
|
stepDone("firewall");
|
||||||
step("post-install env");
|
step("post-install env");
|
||||||
await writePostInstallEnv(context);
|
await writePostInstallEnv(context);
|
||||||
|
stepDone("post-install env");
|
||||||
step("bootstrap admin secret");
|
step("bootstrap admin secret");
|
||||||
await writeBootstrapAdminSecret(context);
|
await writeBootstrapAdminSecret(context);
|
||||||
|
stepDone("bootstrap admin secret");
|
||||||
step("smoke checks");
|
step("smoke checks");
|
||||||
await smoke(context);
|
await smoke(context);
|
||||||
|
stepDone("smoke checks");
|
||||||
step("finalize firewall rollback guard");
|
step("finalize firewall rollback guard");
|
||||||
await cancelFirewallRollback(context);
|
await cancelFirewallRollback(context);
|
||||||
|
stepDone("finalize firewall rollback guard");
|
||||||
step("mark install successful");
|
step("mark install successful");
|
||||||
await markInstallSuccessful(context);
|
await markInstallSuccessful(context);
|
||||||
|
stepDone("mark install successful");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await rollbackFailedInstall(context, state);
|
await rollbackFailedInstall(context, state);
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { ReconfigureContext, ReconfigureOptions } from "../types/context";
|
import type { ReconfigureContext, ReconfigureOptions } from "../types/context";
|
||||||
import { exists, readText, writeText } from "../lib/fs";
|
import { exists, readText, writeText } from "../lib/fs";
|
||||||
import { info, step } from "../lib/log";
|
import { info, setOperationContext, step, stepDone } from "../lib/log";
|
||||||
import { parseRuntimeEnv, renderRuntimeEnv } from "../config/env";
|
import { parseRuntimeEnv, renderRuntimeEnv } from "../config/env";
|
||||||
import { preflight } from "../steps/preflight";
|
import { preflight } from "../steps/preflight";
|
||||||
import { generateConfig } from "../steps/config";
|
import { generateConfig } from "../steps/config";
|
||||||
@@ -79,6 +79,7 @@ async function warnBootstrapDrift(nextConfigRaw: string): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function reconfigure(options: ReconfigureOptions): Promise<void> {
|
export async function reconfigure(options: ReconfigureOptions): Promise<void> {
|
||||||
|
setOperationContext(`reconfigure-${Date.now().toString(36)}`);
|
||||||
const configRaw = await readText(options.sourceConfigPath);
|
const configRaw = await readText(options.sourceConfigPath);
|
||||||
const config = parseRuntimeEnv(configRaw);
|
const config = parseRuntimeEnv(configRaw);
|
||||||
|
|
||||||
@@ -94,8 +95,10 @@ export async function reconfigure(options: ReconfigureOptions): Promise<void> {
|
|||||||
|
|
||||||
step("preflight");
|
step("preflight");
|
||||||
await preflight(context);
|
await preflight(context);
|
||||||
|
stepDone("preflight");
|
||||||
step("install state marker");
|
step("install state marker");
|
||||||
await ensureInstallStateExists();
|
await ensureInstallStateExists();
|
||||||
|
stepDone("install state marker");
|
||||||
await warnBootstrapDrift(configRaw);
|
await warnBootstrapDrift(configRaw);
|
||||||
|
|
||||||
if (options.dryRun) {
|
if (options.dryRun) {
|
||||||
@@ -110,23 +113,30 @@ export async function reconfigure(options: ReconfigureOptions): Promise<void> {
|
|||||||
|
|
||||||
step("backup");
|
step("backup");
|
||||||
await backupCurrentState();
|
await backupCurrentState();
|
||||||
|
stepDone("backup");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
step("config generation");
|
step("config generation");
|
||||||
await generateConfig(context);
|
await generateConfig(context);
|
||||||
|
stepDone("config generation");
|
||||||
step("systemd units");
|
step("systemd units");
|
||||||
await deploySystemd(context);
|
await deploySystemd(context);
|
||||||
|
stepDone("systemd units");
|
||||||
step("firewall");
|
step("firewall");
|
||||||
await applyFirewall(context);
|
await applyFirewall(context);
|
||||||
|
stepDone("firewall");
|
||||||
step("write env artifacts");
|
step("write env artifacts");
|
||||||
await writeText(options.runtimeConfigPath, renderRuntimeEnv(config), 0o600);
|
await writeText(options.runtimeConfigPath, renderRuntimeEnv(config), 0o600);
|
||||||
await runVisible`chown root:root ${options.runtimeConfigPath}`;
|
await runVisible`chown root:root ${options.runtimeConfigPath}`;
|
||||||
await runVisible`chmod 0600 ${options.runtimeConfigPath}`;
|
await runVisible`chmod 0600 ${options.runtimeConfigPath}`;
|
||||||
await writePostInstallEnv(context);
|
await writePostInstallEnv(context);
|
||||||
|
stepDone("write env artifacts");
|
||||||
step("smoke checks");
|
step("smoke checks");
|
||||||
await smoke(context);
|
await smoke(context);
|
||||||
|
stepDone("smoke checks");
|
||||||
step("finalize firewall rollback guard");
|
step("finalize firewall rollback guard");
|
||||||
await cancelFirewallRollback(context);
|
await cancelFirewallRollback(context);
|
||||||
|
stepDone("finalize firewall rollback guard");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
info("reconfigure failed, rollback in progress");
|
info("reconfigure failed, rollback in progress");
|
||||||
await rollbackFirewallNow(context);
|
await rollbackFirewallNow(context);
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import type { CommonOptions } from "../types/context";
|
||||||
|
import { exists, readText } from "../lib/fs";
|
||||||
|
import { info, setOperationContext } from "../lib/log";
|
||||||
|
import { run } from "../lib/process";
|
||||||
|
import { getPlatformProfile } from "../platform/profile";
|
||||||
|
|
||||||
|
async function unitState(unit: string): Promise<string> {
|
||||||
|
try {
|
||||||
|
const out = await run`systemctl is-active ${unit}`;
|
||||||
|
return out.trim() || "unknown";
|
||||||
|
} catch {
|
||||||
|
return "inactive";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function firewallState(): Promise<string> {
|
||||||
|
try {
|
||||||
|
await run`nft -c -f /etc/nftables.conf`;
|
||||||
|
return "valid";
|
||||||
|
} catch {
|
||||||
|
return "invalid";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function tlsState(): Promise<string> {
|
||||||
|
if (!(await exists("/etc/hysteria/config.yaml"))) {
|
||||||
|
return "missing-config";
|
||||||
|
}
|
||||||
|
const cfg = await readText("/etc/hysteria/config.yaml");
|
||||||
|
if (/acme:/m.test(cfg)) {
|
||||||
|
return "acme";
|
||||||
|
}
|
||||||
|
if (/^tls:/m.test(cfg)) {
|
||||||
|
return "file/self-signed";
|
||||||
|
}
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function status(_options: CommonOptions): Promise<void> {
|
||||||
|
setOperationContext(`status-${Date.now().toString(36)}`);
|
||||||
|
const platform = await getPlatformProfile();
|
||||||
|
const result = {
|
||||||
|
ts: new Date().toISOString(),
|
||||||
|
platform,
|
||||||
|
services: {
|
||||||
|
hysteria: await unitState("hysteria-server"),
|
||||||
|
admin: await unitState("hy2xs-admin")
|
||||||
|
},
|
||||||
|
firewall: await firewallState(),
|
||||||
|
tls: await tlsState(),
|
||||||
|
install_state_present: await exists("/var/lib/hy2xs/install-state.json")
|
||||||
|
};
|
||||||
|
info(`status report: ${JSON.stringify(result)}`);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,11 +1,45 @@
|
|||||||
|
let operationId = "";
|
||||||
|
|
||||||
|
function nowIso(): string {
|
||||||
|
return new Date().toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureOperationId(): string {
|
||||||
|
if (!operationId) {
|
||||||
|
operationId = `op-${Date.now().toString(36)}`;
|
||||||
|
}
|
||||||
|
return operationId;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setOperationContext(id: string): void {
|
||||||
|
operationId = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
function emit(level: "STEP" | "INFO" | "ERROR", message: string, stepName?: string, status?: "start" | "ok" | "fail"): void {
|
||||||
|
const payload = {
|
||||||
|
ts: nowIso(),
|
||||||
|
op_id: ensureOperationId(),
|
||||||
|
level,
|
||||||
|
step: stepName ?? null,
|
||||||
|
status: status ?? null,
|
||||||
|
message
|
||||||
|
};
|
||||||
|
console.log(`[hy2xs] ${JSON.stringify(payload)}`);
|
||||||
|
}
|
||||||
|
|
||||||
export function step(name: string): void {
|
export function step(name: string): void {
|
||||||
console.log(`\n[hy2xs] ==> ${name}`);
|
emit("STEP", `==> ${name}`, name, "start");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stepDone(name: string): void {
|
||||||
|
emit("STEP", `<== ${name}`, name, "ok");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function info(message: string): void {
|
export function info(message: string): void {
|
||||||
console.log(`[hy2xs] ${message}`);
|
emit("INFO", message);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fail(message: string): never {
|
export function fail(message: string): never {
|
||||||
|
emit("ERROR", message, undefined, "fail");
|
||||||
throw new Error(message);
|
throw new Error(message);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { fail } from "../lib/log";
|
||||||
|
import { getPlatformProfile } from "./profile";
|
||||||
|
|
||||||
|
type AssertPlatformOptions = {
|
||||||
|
distro: "debian";
|
||||||
|
supportedVersions: number[];
|
||||||
|
architectures: Array<"amd64">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function assertPlatform(options: AssertPlatformOptions): Promise<void> {
|
||||||
|
const profile = await getPlatformProfile();
|
||||||
|
|
||||||
|
if (profile.distro !== options.distro) {
|
||||||
|
fail(`HY2XS baseline supports only ${options.distro}; detected: ${profile.distro || "unknown"}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!options.supportedVersions.includes(profile.majorVersion)) {
|
||||||
|
fail(
|
||||||
|
`HY2XS baseline supports only ${options.distro} ${options.supportedVersions.join(", ")}; detected major version: ${profile.majorVersion || "unknown"}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (profile.architecture !== "amd64" || !options.architectures.includes("amd64")) {
|
||||||
|
fail(`HY2XS baseline supports only ${options.architectures.join(",")}; detected: ${profile.architecture}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!profile.capabilities.systemd) {
|
||||||
|
fail("required capability missing: systemd");
|
||||||
|
}
|
||||||
|
if (!profile.capabilities.systemdRun) {
|
||||||
|
fail("required capability missing: systemd-run");
|
||||||
|
}
|
||||||
|
if (!profile.capabilities.nftables) {
|
||||||
|
fail("required capability missing: nft");
|
||||||
|
}
|
||||||
|
if (!profile.capabilities.nftAtomicReplace) {
|
||||||
|
fail("required capability missing: nft atomic replace");
|
||||||
|
}
|
||||||
|
if (!profile.capabilities.openssl3) {
|
||||||
|
fail("required capability missing: OpenSSL 3.x runtime");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import { exists, readText } from "../lib/fs";
|
||||||
|
import { run } from "../lib/process";
|
||||||
|
|
||||||
|
export type PlatformProfile = {
|
||||||
|
distro: string;
|
||||||
|
majorVersion: number;
|
||||||
|
architecture: "amd64" | "unsupported";
|
||||||
|
capabilities: {
|
||||||
|
nftables: boolean;
|
||||||
|
systemd: boolean;
|
||||||
|
openssl3: boolean;
|
||||||
|
systemdRun: boolean;
|
||||||
|
nftAtomicReplace: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
function parseOsRelease(content: string): Record<string, string> {
|
||||||
|
const result: Record<string, string> = {};
|
||||||
|
for (const rawLine of content.split("\n")) {
|
||||||
|
const line = rawLine.trim();
|
||||||
|
if (!line || line.startsWith("#")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const index = line.indexOf("=");
|
||||||
|
if (index <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const key = line.slice(0, index);
|
||||||
|
let value = line.slice(index + 1);
|
||||||
|
value = value.replace(/^"(.*)"$/, "$1");
|
||||||
|
result[key] = value;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function commandExists(command: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await run`command -v ${command} >/dev/null 2>&1`;
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function detectOpenSsl3(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const output = await run`openssl version`;
|
||||||
|
return /^OpenSSL\s+3\./.test(output);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getPlatformProfile(): Promise<PlatformProfile> {
|
||||||
|
const osReleaseRaw = await readText("/etc/os-release");
|
||||||
|
const parsed = parseOsRelease(osReleaseRaw);
|
||||||
|
const distro = (parsed.ID || "").toLowerCase();
|
||||||
|
const majorVersion = Number.parseInt((parsed.VERSION_ID || "").replace(/"/g, ""), 10);
|
||||||
|
|
||||||
|
const archRaw = await run`uname -m`;
|
||||||
|
const architecture: PlatformProfile["architecture"] = archRaw.trim() === "x86_64" ? "amd64" : "unsupported";
|
||||||
|
|
||||||
|
const nftables = await commandExists("nft");
|
||||||
|
const systemd = await commandExists("systemctl") && (await exists("/run/systemd/system"));
|
||||||
|
const systemdRun = await commandExists("systemd-run");
|
||||||
|
const openssl3 = await detectOpenSsl3();
|
||||||
|
const nftAtomicReplace = nftables;
|
||||||
|
|
||||||
|
return {
|
||||||
|
distro,
|
||||||
|
majorVersion,
|
||||||
|
architecture,
|
||||||
|
capabilities: {
|
||||||
|
nftables,
|
||||||
|
systemd,
|
||||||
|
openssl3,
|
||||||
|
systemdRun,
|
||||||
|
nftAtomicReplace
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import type { RuntimeContext } from "../types/context";
|
import type { RuntimeContext } from "../types/context";
|
||||||
import { exists, readText } from "../lib/fs";
|
import { exists } from "../lib/fs";
|
||||||
import { fail, info } from "../lib/log";
|
import { fail, info } from "../lib/log";
|
||||||
import { run } from "../lib/process";
|
import { run } from "../lib/process";
|
||||||
|
import { assertPlatform } from "../platform/assert";
|
||||||
|
|
||||||
async function isTcpPortListening(port: number): Promise<boolean> {
|
async function isTcpPortListening(port: number): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
@@ -43,10 +44,11 @@ export async function preflight(context: RuntimeContext): Promise<void> {
|
|||||||
fail("sudo is required for installer smoke checks. Install it with: apt-get update && apt-get install -y sudo");
|
fail("sudo is required for installer smoke checks. Install it with: apt-get update && apt-get install -y sudo");
|
||||||
}
|
}
|
||||||
|
|
||||||
const osRelease = await readText("/etc/os-release");
|
await assertPlatform({
|
||||||
if (!/^ID=debian$/m.test(osRelease) || !/^VERSION_ID="?12"?$/m.test(osRelease)) {
|
distro: "debian",
|
||||||
fail("HY2XS baseline supports only clean Debian 12");
|
supportedVersions: [13],
|
||||||
}
|
architectures: ["amd64"]
|
||||||
|
});
|
||||||
|
|
||||||
if (!(await exists(`${context.options.packageDir}/systemd/hy2xs-admin.service`))) {
|
if (!(await exists(`${context.options.packageDir}/systemd/hy2xs-admin.service`))) {
|
||||||
fail("missing hy2xs-admin systemd unit in package");
|
fail("missing hy2xs-admin systemd unit in package");
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# HY2XS install package
|
# HY2XS install package
|
||||||
|
|
||||||
Этот пакет создаётся production builder'ом на Debian 12 amd64.
|
Этот пакет создаётся production builder'ом на Debian 13 amd64.
|
||||||
|
|
||||||
Пакет предназначен для чистого Debian 12 target и содержит:
|
Пакет предназначен для чистого Debian 13 target и содержит:
|
||||||
|
|
||||||
- compiled install-only orchestrator;
|
- compiled install-only orchestrator;
|
||||||
- bundled HY2XS admin;
|
- bundled HY2XS admin;
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
# HY2XS post-install reference file.
|
# HY2XS post-install reference file.
|
||||||
# Файл создаётся оркестратором после первичной установки и не является runtime-конфигом.
|
# Файл создаётся оркестратором после первичной установки и не является runtime-конфигом.
|
||||||
|
|
||||||
DEPLOY_TARGET_OS=debian12
|
DEPLOY_TARGET_OS=debian13
|
||||||
DEPLOY_TIMESTAMP={{LAST_APPLY_DATE}}
|
DEPLOY_TIMESTAMP={{LAST_APPLY_DATE}}
|
||||||
PACKAGE_NAME=hy2xs-install-package
|
PACKAGE_NAME=hy2xs-install-package
|
||||||
PACKAGE_BUILD_ID={{PACKAGE_BUILD_ID}}
|
PACKAGE_BUILD_ID={{PACKAGE_BUILD_ID}}
|
||||||
|
|||||||
Reference in New Issue
Block a user