Files
HY2XS_flamy/orchestrator/src/commands/reconfigure.ts
T

137 lines
6.8 KiB
TypeScript

import type { ReconfigureContext, ReconfigureOptions } from "../types/context";
import { exists, readText, writeText } from "../lib/fs";
import { info, step } from "../lib/log";
import { parseRuntimeEnv, renderRuntimeEnv } from "../config/env";
import { preflight } from "../steps/preflight";
import { generateConfig } from "../steps/config";
import { deploySystemd } from "../steps/systemd";
import { applyFirewall, cancelFirewallRollback, rollbackFirewallNow } from "../steps/firewall";
import { writePostInstallEnv } from "../steps/env";
import { smoke } from "../steps/smoke";
import { runVisible } from "../lib/process";
import { readInstalledHysteriaVersion, readPackageValue } from "../lib/packageMeta";
const INSTALL_STATE_PATH = "/var/lib/hy2xs/install-state.json";
type InstallState = {
installed?: boolean;
};
async function backupCurrentState(): Promise<void> {
await runVisible`mkdir -p /etc/hy2xs/backups`;
await runVisible`cp -a /etc/hysteria/config.yaml /etc/hy2xs/backups/config.yaml.bak 2>/dev/null || true`;
await runVisible`cp -a /etc/systemd/system/hy2xs-admin.service /etc/hy2xs/backups/hy2xs-admin.service.bak 2>/dev/null || true`;
await runVisible`cp -a /etc/systemd/system/hysteria-server.service /etc/hy2xs/backups/hysteria-server.service.bak 2>/dev/null || true`;
await runVisible`cp -a /etc/hy2xs/hy2xs.env /etc/hy2xs/backups/hy2xs.env.bak 2>/dev/null || true`;
await runVisible`cp -a /etc/hysteria/post-install.env /etc/hy2xs/backups/post-install.env.bak 2>/dev/null || true`;
await runVisible`cp -a /etc/nftables.conf /etc/hy2xs/backups/nftables.conf.bak 2>/dev/null || true`;
await runVisible`cp -a /etc/nftables.d/hy2xs.nft /etc/hy2xs/backups/hy2xs.nft.bak 2>/dev/null || true`;
await runVisible`test -f /etc/hy2xs/hy2xs.env && echo 1 > /etc/hy2xs/backups/hy2xs.env.existed || rm -f /etc/hy2xs/backups/hy2xs.env.existed`;
await runVisible`test -f /etc/hysteria/post-install.env && echo 1 > /etc/hy2xs/backups/post-install.env.existed || rm -f /etc/hy2xs/backups/post-install.env.existed`;
await runVisible`test -f /etc/nftables.conf && echo 1 > /etc/hy2xs/backups/nftables.conf.existed || rm -f /etc/hy2xs/backups/nftables.conf.existed`;
await runVisible`test -f /etc/nftables.d/hy2xs.nft && echo 1 > /etc/hy2xs/backups/hy2xs.nft.existed || rm -f /etc/hy2xs/backups/hy2xs.nft.existed`;
}
async function rollbackCurrentState(): Promise<void> {
await runVisible`cp -a /etc/hy2xs/backups/config.yaml.bak /etc/hysteria/config.yaml 2>/dev/null || true`;
await runVisible`cp -a /etc/hy2xs/backups/hy2xs-admin.service.bak /etc/systemd/system/hy2xs-admin.service 2>/dev/null || true`;
await runVisible`cp -a /etc/hy2xs/backups/hysteria-server.service.bak /etc/systemd/system/hysteria-server.service 2>/dev/null || true`;
await runVisible`if [ -f /etc/hy2xs/backups/hy2xs.env.existed ]; then cp -a /etc/hy2xs/backups/hy2xs.env.bak /etc/hy2xs/hy2xs.env 2>/dev/null || true; else rm -f /etc/hy2xs/hy2xs.env; fi`;
await runVisible`if [ -f /etc/hy2xs/backups/post-install.env.existed ]; then cp -a /etc/hy2xs/backups/post-install.env.bak /etc/hysteria/post-install.env 2>/dev/null || true; else rm -f /etc/hysteria/post-install.env; fi`;
await runVisible`if [ -f /etc/hy2xs/backups/nftables.conf.existed ]; then cp -a /etc/hy2xs/backups/nftables.conf.bak /etc/nftables.conf 2>/dev/null || true; fi`;
await runVisible`if [ -f /etc/hy2xs/backups/hy2xs.nft.existed ]; then cp -a /etc/hy2xs/backups/hy2xs.nft.bak /etc/nftables.d/hy2xs.nft 2>/dev/null || true; else rm -f /etc/nftables.d/hy2xs.nft; fi`;
await runVisible`nft -f /etc/nftables.conf >/dev/null 2>&1 || true`;
await runVisible`systemctl daemon-reload`;
await runVisible`systemctl restart hysteria-server hy2xs-admin || true`;
}
async function ensureInstallStateExists(): Promise<void> {
if (!(await exists(INSTALL_STATE_PATH))) {
throw new Error(`install state marker is missing: ${INSTALL_STATE_PATH}. Run install first.`);
}
const raw = await readText(INSTALL_STATE_PATH);
let parsed: InstallState;
try {
parsed = JSON.parse(raw) as InstallState;
} catch {
throw new Error(`invalid install state marker format: ${INSTALL_STATE_PATH}`);
}
if (!parsed.installed) {
throw new Error(`install state marker does not indicate successful installation: ${INSTALL_STATE_PATH}`);
}
}
async function warnBootstrapDrift(nextConfigRaw: string): Promise<void> {
if (!(await exists("/etc/hy2xs/hy2xs.env"))) {
return;
}
const prev = parseRuntimeEnv(await readText("/etc/hy2xs/hy2xs.env"));
const next = parseRuntimeEnv(nextConfigRaw);
if (prev.adminInitialPassword !== next.adminInitialPassword || prev.adminConPass !== next.adminConPass) {
info("warning: Admin bootstrap fields are install-only and will not rotate existing credentials.");
info("warning: Use a dedicated password rotation flow in application/account layer.");
}
}
export async function reconfigure(options: ReconfigureOptions): Promise<void> {
const configRaw = await readText(options.sourceConfigPath);
const config = parseRuntimeEnv(configRaw);
const context: ReconfigureContext = {
mode: "reconfigure",
options,
config,
packageVersion: await readPackageValue(options.packageDir, "package.version", "unknown"),
packageBuildId: await readPackageValue(options.packageDir, "package.build_id", "unknown"),
installDate: new Date().toISOString(),
hysteriaVersion: await readInstalledHysteriaVersion()
};
step("preflight");
await preflight(context);
step("install state marker");
await ensureInstallStateExists();
await warnBootstrapDrift(configRaw);
if (options.dryRun) {
info("reconfigure dry-run: validated config and execution graph");
info(`config source: ${options.sourceConfigPath}`);
info(`runtime file: ${options.runtimeConfigPath}`);
info(`ui bind: ${config.uiBindHost}:${config.uiPort}`);
info(`hysteria bind: ${config.hysteriaBindHost}:${config.hysteriaPort}`);
info(`public endpoint: ${config.publicHost}:${config.publicPort}`);
return;
}
step("backup");
await backupCurrentState();
try {
step("config generation");
await generateConfig(context);
step("systemd units");
await deploySystemd(context);
step("firewall");
await applyFirewall(context);
step("write env artifacts");
await writeText(options.runtimeConfigPath, renderRuntimeEnv(config), 0o600);
await runVisible`chown root:root ${options.runtimeConfigPath}`;
await runVisible`chmod 0600 ${options.runtimeConfigPath}`;
await writePostInstallEnv(context);
step("smoke checks");
await smoke(context);
step("finalize firewall rollback guard");
await cancelFirewallRollback(context);
} catch (error) {
info("reconfigure failed, rollback in progress");
await rollbackFirewallNow(context);
await rollbackCurrentState();
throw error;
}
}