- Add node_modules/, test-results/, e2e-report/, .omp/ to .gitignore - Remove throwaway debug scripts (debug-pw*.js, debug-login-dom.js) - Remove empty mock-server/ directory - Untrack harness artifact (.omp/plans/) - Add missing project files to git (e2e tests, Dockerfiles, tooling configs)
303 lines
7.4 KiB
Ucode
303 lines
7.4 KiB
Ucode
'use strict';
|
|
|
|
let uci = require('uci');
|
|
let fs = require('fs');
|
|
|
|
const SWITCH_SCRIPT = '/usr/sbin/zt-gateway-switch';
|
|
const DRAIN_PIDFILE = '/var/run/zt-gateway-drain.pid';
|
|
const DRAIN_NEWFILE = '/var/run/zt-gateway-drain.new';
|
|
const DRAIN_OLDFILE = '/var/run/zt-gateway-drain.old';
|
|
|
|
function load_gateways() {
|
|
const cursor = uci.cursor();
|
|
const result = [];
|
|
cursor.foreach('zt-gateway', 'gateway', function(s) {
|
|
push(result, {
|
|
region: s.region ?? '',
|
|
label: s.label ?? s.region ?? '',
|
|
ip: s.ip ?? '',
|
|
default: s.default === '1',
|
|
health_check: s.health_check ?? 'ping'
|
|
});
|
|
});
|
|
return result;
|
|
}
|
|
function gateway_by_region(region) {
|
|
const gws = load_gateways();
|
|
for (let i = 0; i < length(gws); i++) {
|
|
if (gws[i].region === region) {
|
|
return gws[i];
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function read_active_region() {
|
|
const cursor = uci.cursor();
|
|
return cursor.get('zt-gateway', 'global', 'active_gateway') ?? '';
|
|
}
|
|
|
|
function read_global_option(option, default_value) {
|
|
const cursor = uci.cursor();
|
|
return cursor.get('zt-gateway', 'global', option) ?? default_value;
|
|
}
|
|
|
|
function drain_active() {
|
|
const st = fs.stat(DRAIN_PIDFILE);
|
|
return st?.type === 'file';
|
|
}
|
|
|
|
function read_drain_pid() {
|
|
const data = fs.readfile(DRAIN_PIDFILE);
|
|
if (!data) {
|
|
return null;
|
|
}
|
|
const s = trim(data);
|
|
return length(s) > 0 ? +s : null;
|
|
}
|
|
|
|
function read_drain_state() {
|
|
let new_ip = '';
|
|
let old_ip = '';
|
|
const n = fs.readfile(DRAIN_NEWFILE);
|
|
if (n) {
|
|
new_ip = trim(n);
|
|
}
|
|
const o = fs.readfile(DRAIN_OLDFILE);
|
|
if (o) {
|
|
old_ip = trim(o);
|
|
}
|
|
return { new_ip, old_ip };
|
|
}
|
|
|
|
|
|
function shell_quote(s) {
|
|
if (!s) {
|
|
return "''";
|
|
}
|
|
if (match(s, /^[A-Za-z0-9_./:@=-]+$/) != null) {
|
|
return s;
|
|
}
|
|
return `'${replace(s, "'", "'\\''")}'`;
|
|
}
|
|
|
|
function run_switch_script_capture(ip, mode) {
|
|
const mode_lc = mode ?? 'force';
|
|
let cmd = `${SWITCH_SCRIPT} ${shell_quote(ip)} ${shell_quote(mode_lc)}`;
|
|
if (mode_lc === 'graceful') {
|
|
const to = +read_global_option('drain_timeout', '600');
|
|
cmd += ` ${to > 0 ? to : 600}`;
|
|
}
|
|
const stderr_pipe = '/tmp/zt-gw-stderr';
|
|
const code = system(`${cmd} 2>${stderr_pipe}`);
|
|
const err = fs.readfile(stderr_pipe) ?? '';
|
|
fs.unlink(stderr_pipe);
|
|
return { code: code ?? 0, stderr: trim(err) };
|
|
}
|
|
|
|
function run_switch_script_force_capture(ip) {
|
|
const stderr_pipe = '/tmp/zt-gw-stderr';
|
|
const code = system(`${SWITCH_SCRIPT} ${shell_quote(ip)} force 2>${stderr_pipe}`);
|
|
const err = fs.readfile(stderr_pipe) ?? '';
|
|
fs.unlink(stderr_pipe);
|
|
return { code: code ?? 0, stderr: trim(err) };
|
|
}
|
|
|
|
function system_output(cmd) {
|
|
const tmp = `/tmp/zt-gw-out-${time()}`;
|
|
system(`${cmd} >${tmp} 2>/dev/null`);
|
|
const out = fs.readfile(tmp) ?? '';
|
|
fs.unlink(tmp);
|
|
return out;
|
|
}
|
|
|
|
return {
|
|
'zt-gateway': {
|
|
status: {
|
|
call: function(req) {
|
|
const gws = load_gateways();
|
|
const active_region = read_active_region();
|
|
|
|
let active_ip = '';
|
|
const active_gw = gateway_by_region(active_region);
|
|
if (active_gw) {
|
|
active_ip = active_gw.ip;
|
|
}
|
|
|
|
const drain_state = read_drain_state();
|
|
let remaining = 0;
|
|
if (drain_active()) {
|
|
const out = system_output(`conntrack -L -m 0x100 2>/dev/null | wc -l`);
|
|
remaining = +trim(out) || 0;
|
|
}
|
|
|
|
const cursor = uci.cursor();
|
|
const settings = {
|
|
switch_mode: cursor.get('zt-gateway', 'global', 'switch_mode') ?? 'force',
|
|
drain_timeout: +(cursor.get('zt-gateway', 'global', 'drain_timeout') ?? 600),
|
|
health_interval: +(cursor.get('zt-gateway', 'global', 'health_interval') ?? 60),
|
|
auto_failback: cursor.get('zt-gateway', 'global', 'auto_failback') === '1'
|
|
};
|
|
|
|
return {
|
|
active_gateway: active_region,
|
|
active_ip: active_ip,
|
|
settings: settings,
|
|
gateways: gws,
|
|
drain: {
|
|
active: drain_active(),
|
|
new_ip: drain_state.new_ip,
|
|
old_ip: drain_state.old_ip,
|
|
remaining_connections: remaining
|
|
}
|
|
};
|
|
}
|
|
},
|
|
|
|
switch: {
|
|
args: {
|
|
region: 'string',
|
|
mode: 'string'
|
|
},
|
|
call: function(req) {
|
|
const region = req.args?.region;
|
|
const mode = req.args?.mode ?? read_global_option('switch_mode', 'force');
|
|
if (!region || (mode !== 'force' && mode !== 'graceful')) {
|
|
return {
|
|
success: false,
|
|
message: 'Missing or invalid argument: region required, mode must be force|graceful'
|
|
};
|
|
}
|
|
|
|
if (drain_active() && mode === 'graceful') {
|
|
return {
|
|
success: false,
|
|
message: 'A graceful drain is already in progress. Cancel it first.'
|
|
};
|
|
}
|
|
|
|
const gw = gateway_by_region(region);
|
|
if (!gw) {
|
|
return { success: false, message: `Unknown gateway region: ${region}` };
|
|
}
|
|
|
|
const current_region = read_active_region();
|
|
if (current_region === region && !drain_active()) {
|
|
return { success: true, message: `Already on ${region}.` };
|
|
}
|
|
|
|
const result = run_switch_script_capture(gw.ip, mode);
|
|
if (result.code !== 0) {
|
|
return {
|
|
success: false,
|
|
message: result.stderr || `Switch script failed with exit code ${result.code}`
|
|
};
|
|
}
|
|
|
|
const cursor = uci.cursor();
|
|
cursor.set('zt-gateway', 'global', 'active_gateway', region);
|
|
cursor.commit('zt-gateway');
|
|
|
|
return {
|
|
success: true,
|
|
message: `Switched to ${region} (${gw.ip}) via ${mode}`,
|
|
active_gateway: region,
|
|
mode: mode,
|
|
drain_active: (mode === 'graceful')
|
|
};
|
|
}
|
|
},
|
|
|
|
health: {
|
|
args: {
|
|
region: 'string'
|
|
},
|
|
call: function(req) {
|
|
const region = req.args?.region;
|
|
if (!region) {
|
|
return { reachable: false, message: 'Missing region' };
|
|
}
|
|
|
|
const gw = gateway_by_region(region);
|
|
if (!gw) {
|
|
return { reachable: false, message: `Unknown region ${region}` };
|
|
}
|
|
|
|
const out = system_output(
|
|
`ping -c 2 -W 3 -I br-zt ${shell_quote(gw.ip)} 2>/dev/null; echo "EXIT:$?"`
|
|
);
|
|
const exit_match = match(out, /EXIT:(\d+)/);
|
|
const exit_code = exit_match ? +exit_match[1] : 1;
|
|
|
|
let latency_ms = null;
|
|
if (exit_code === 0) {
|
|
const stats = match(out, /rtt min\/avg\/max[^=]*=\s*[\d.]+\/([\d.]+)\//);
|
|
if (stats) {
|
|
latency_ms = +stats[1];
|
|
}
|
|
}
|
|
|
|
return {
|
|
region: region,
|
|
ip: gw.ip,
|
|
reachable: exit_code === 0,
|
|
latency_ms: latency_ms
|
|
};
|
|
}
|
|
},
|
|
|
|
drain_status: {
|
|
call: function(req) {
|
|
const active = drain_active();
|
|
const state = read_drain_state();
|
|
let remaining = 0;
|
|
if (active) {
|
|
const out = system_output(`conntrack -L -m 0x100 2>/dev/null | wc -l`);
|
|
remaining = +trim(out) || 0;
|
|
}
|
|
return {
|
|
active: active,
|
|
new_ip: state.new_ip,
|
|
old_ip: state.old_ip,
|
|
remaining_connections: remaining
|
|
};
|
|
}
|
|
},
|
|
|
|
cancel_drain: {
|
|
call: function(req) {
|
|
if (!drain_active()) {
|
|
return { success: false, message: 'No drain in progress' };
|
|
}
|
|
|
|
const state = read_drain_state();
|
|
const pid = read_drain_pid();
|
|
if (pid) {
|
|
system(`kill -TERM ${pid} 2>/dev/null`);
|
|
}
|
|
|
|
if (!state.new_ip) {
|
|
fs.unlink(DRAIN_PIDFILE);
|
|
return {
|
|
success: true,
|
|
message: 'Drain cancelled (new gateway unknown; route left as-is).'
|
|
};
|
|
}
|
|
|
|
const result = run_switch_script_force_capture(state.new_ip);
|
|
|
|
fs.unlink(DRAIN_PIDFILE);
|
|
fs.unlink(DRAIN_NEWFILE);
|
|
fs.unlink(DRAIN_OLDFILE);
|
|
|
|
return {
|
|
success: result.code === 0,
|
|
message: result.code === 0
|
|
? `Drain cancelled; force-switched to ${state.new_ip}.`
|
|
: (result.stderr || `Cancel failed with code ${result.code}`)
|
|
};
|
|
}
|
|
}
|
|
}
|
|
};
|