Add CultMesh operator notification publisher
This commit is contained in:
parent
5a1a9098de
commit
755649ff29
1 changed files with 242 additions and 0 deletions
242
tools/operator-notification.mjs
Normal file
242
tools/operator-notification.mjs
Normal file
|
|
@ -0,0 +1,242 @@
|
|||
#!/usr/bin/env node
|
||||
import { createRequire } from "node:module";
|
||||
import { existsSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { createHash } from "node:crypto";
|
||||
|
||||
const repoRoot = resolve(import.meta.dirname, "..");
|
||||
const projectsRoot = resolve(repoRoot, "..");
|
||||
const defaultStorePath = resolve(projectsRoot, "VoidBot", ".voidbot", "status", "cultmesh", "voidbot-swarm-state.cc");
|
||||
const requestDocumentType = "gamecult.operator_dm_request";
|
||||
const requestSchemaId = "gamecult.operator_dm_request.v1";
|
||||
|
||||
async function main() {
|
||||
const [command, ...rawArgs] = process.argv.slice(2);
|
||||
const options = parseArgs(rawArgs);
|
||||
|
||||
if (!command || command === "help" || command === "--help" || command === "-h") {
|
||||
printHelp();
|
||||
return;
|
||||
}
|
||||
|
||||
switch (command) {
|
||||
case "publish-idunn-alarm":
|
||||
await publishIdunnAlarm(options);
|
||||
return;
|
||||
default:
|
||||
throw new Error(`Unknown command "${command}". Run "node tools/operator-notification.mjs help".`);
|
||||
}
|
||||
}
|
||||
|
||||
async function publishIdunnAlarm(options) {
|
||||
const storePath = resolve(options.store ?? defaultStorePath);
|
||||
const alarm = {
|
||||
alarmId: readOptionOrEnv(options, "alarm-id", "IDUNN_ALARM_ID", "unknown-alarm"),
|
||||
daemonId: readOptionOrEnv(options, "daemon-id", "IDUNN_ALARM_DAEMON_ID", "unknown-daemon"),
|
||||
severity: readOptionOrEnv(options, "severity", "IDUNN_ALARM_SEVERITY", "operator-action-required"),
|
||||
reason: readOptionOrEnv(options, "reason", "IDUNN_ALARM_REASON", "Idunn raised an operator alarm."),
|
||||
raisedAt: readOptionOrEnv(options, "raised-at", "IDUNN_ALARM_RAISED_AT", new Date().toISOString()),
|
||||
};
|
||||
const request = buildOperatorDmRequest(alarm);
|
||||
|
||||
if (options["dry-run"] === "true") {
|
||||
printJson({
|
||||
dryRun: true,
|
||||
action: "publish-idunn-alarm",
|
||||
storePath,
|
||||
documentType: requestDocumentType,
|
||||
schemaId: requestSchemaId,
|
||||
request,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const { CultMesh, defineDocumentType } = loadCultMeshRuntime();
|
||||
const requestDefinition = defineDocumentType({
|
||||
type: requestDocumentType,
|
||||
schemaName: requestDocumentType,
|
||||
schemaId: requestSchemaId,
|
||||
schemaVersion: requestSchemaId,
|
||||
contentHash: requestSchemaId,
|
||||
global: false,
|
||||
name: "requestId",
|
||||
schema: { parse: parseObjectDocument("Operator DM request") },
|
||||
});
|
||||
const node = await CultMesh.createNode(storePath, {
|
||||
documents: [
|
||||
requestDefinition,
|
||||
defineDocumentType({
|
||||
type: "voidbot.swarm_state_snapshot",
|
||||
schemaName: "voidbot.swarm_state_snapshot",
|
||||
schemaId: "voidbot.swarm_state_snapshot.v1",
|
||||
schemaVersion: "voidbot.swarm_state_snapshot.v1",
|
||||
contentHash: "voidbot.swarm_state_snapshot.v1",
|
||||
global: false,
|
||||
schema: { parse: parseObjectDocument("VoidBot swarm snapshot") },
|
||||
}),
|
||||
defineDocumentType({
|
||||
type: "gamecult.eve.provider_advertisement",
|
||||
schemaName: "gamecult.eve.provider_advertisement",
|
||||
schemaId: "gamecult.eve.provider_advertisement.v1",
|
||||
schemaVersion: "gamecult.eve.provider_advertisement.v1",
|
||||
contentHash: "gamecult.eve.provider_advertisement.v1",
|
||||
global: false,
|
||||
name: "providerId",
|
||||
schema: { parse: parseObjectDocument("Eve provider advertisement") },
|
||||
}),
|
||||
defineDocumentType({
|
||||
type: "gamecult.eve.surface_state",
|
||||
schemaName: "gamecult.eve.surface_state",
|
||||
schemaId: "gamecult.eve.surface_state.v1",
|
||||
schemaVersion: "gamecult.eve.surface_state.v1",
|
||||
contentHash: "gamecult.eve.surface_state.v1",
|
||||
global: false,
|
||||
name: "providerId",
|
||||
schema: { parse: parseObjectDocument("Eve surface state") },
|
||||
}),
|
||||
defineDocumentType({
|
||||
type: "gamecult.eve.interface_binding",
|
||||
schemaName: "gamecult.eve.interface_binding",
|
||||
schemaId: "gamecult.eve.interface_binding.v1",
|
||||
schemaVersion: "gamecult.eve.interface_binding.v1",
|
||||
contentHash: "gamecult.eve.interface_binding.v1",
|
||||
global: false,
|
||||
name: "bindingId",
|
||||
schema: { parse: parseObjectDocument("Eve interface binding") },
|
||||
}),
|
||||
],
|
||||
});
|
||||
await node.put(requestDefinition, request.requestId, request);
|
||||
await node.flush?.();
|
||||
|
||||
printJson({
|
||||
ok: true,
|
||||
action: "publish-idunn-alarm",
|
||||
storePath,
|
||||
documentType: requestDocumentType,
|
||||
schemaId: requestSchemaId,
|
||||
requestId: request.requestId,
|
||||
});
|
||||
}
|
||||
|
||||
function buildOperatorDmRequest(alarm) {
|
||||
const requestId = `idunn-alarm-${createHash("sha1")
|
||||
.update(JSON.stringify(alarm))
|
||||
.digest("hex")
|
||||
.slice(0, 16)}`;
|
||||
const message = [
|
||||
"Idunn needs operator intervention.",
|
||||
"",
|
||||
`Service: ${alarm.daemonId}`,
|
||||
`Severity: ${alarm.severity}`,
|
||||
`Reason: ${alarm.reason}`,
|
||||
`Alarm: ${alarm.alarmId}`,
|
||||
`Raised: ${alarm.raisedAt}`,
|
||||
"",
|
||||
"Automatic recovery either was not authorized or failed.",
|
||||
].join("\n");
|
||||
|
||||
return {
|
||||
requestId,
|
||||
command: "owner.dm.send",
|
||||
status: "pending",
|
||||
service: alarm.daemonId,
|
||||
sourceId: alarm.alarmId,
|
||||
severity: alarm.severity,
|
||||
reason: alarm.reason,
|
||||
message,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
requestedBy: "idunn",
|
||||
transportOwner: "bifrost",
|
||||
schemaName: requestDocumentType,
|
||||
schemaVersion: requestSchemaId,
|
||||
};
|
||||
}
|
||||
|
||||
function loadCultMeshRuntime() {
|
||||
const candidates = [
|
||||
resolve(projectsRoot, "CultLib", "packages", "cultmesh-ts", "package.json"),
|
||||
resolve(projectsRoot, "CultMeshTS", "package.json"),
|
||||
];
|
||||
|
||||
for (const packageJson of candidates) {
|
||||
if (!existsSync(packageJson)) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const requireCult = createRequire(packageJson);
|
||||
const { CultMesh } = requireCult("cultmesh-ts");
|
||||
const { defineDocumentType } = requireCult("cultcache-ts");
|
||||
if (CultMesh && defineDocumentType) {
|
||||
return { CultMesh, defineDocumentType };
|
||||
}
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error("CultMesh/CultCache TypeScript runtime is unavailable.");
|
||||
}
|
||||
|
||||
function parseObjectDocument(label) {
|
||||
return {
|
||||
parse(input) {
|
||||
if (!input || typeof input !== "object") {
|
||||
throw new Error(`${label} must be an object.`);
|
||||
}
|
||||
return input;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function readOptionOrEnv(options, optionName, envName, fallback) {
|
||||
return optionalString(options[optionName]) ?? optionalString(process.env[envName]) ?? fallback;
|
||||
}
|
||||
|
||||
function parseArgs(args) {
|
||||
const options = {};
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const token = args[index];
|
||||
if (!token.startsWith("--")) {
|
||||
throw new Error(`Unexpected argument "${token}".`);
|
||||
}
|
||||
const key = token.slice(2);
|
||||
const next = args[index + 1];
|
||||
if (!next || next.startsWith("--")) {
|
||||
options[key] = "true";
|
||||
continue;
|
||||
}
|
||||
options[key] = next;
|
||||
index += 1;
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
function optionalString(value) {
|
||||
if (typeof value !== "string") {
|
||||
return undefined;
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
return trimmed.length > 0 ? trimmed : undefined;
|
||||
}
|
||||
|
||||
function printJson(value) {
|
||||
process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
|
||||
}
|
||||
|
||||
function printHelp() {
|
||||
process.stdout.write(`Bifrost operator notification CultMesh publisher
|
||||
|
||||
Commands:
|
||||
publish-idunn-alarm Publish an Idunn alarm as gamecult.operator_dm_request.v1
|
||||
|
||||
Examples:
|
||||
node tools/operator-notification.mjs publish-idunn-alarm --dry-run
|
||||
node tools/operator-notification.mjs publish-idunn-alarm --daemon-id voidbot --reason "restart failed"
|
||||
`);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
Loading…
Reference in a new issue