Initial
This commit is contained in:
257
server/services/systemJobService.js
Normal file
257
server/services/systemJobService.js
Normal file
@@ -0,0 +1,257 @@
|
||||
import { db, now } from '../db.js';
|
||||
import { camelJob } from '../models/mappers.js';
|
||||
import { listSystemJobActions, recordSystemJobAction } from '../models/systemJobModel.js';
|
||||
import { syncDynamicHostGroups } from '../models/hostGroupModel.js';
|
||||
import { runCatalogChecks } from './catalogWorker.js';
|
||||
import { runAutoPromotions } from './promotionWorker.js';
|
||||
import { cancelExecutionJob, appendJobLog, activeExecutionJobIds } from './powershellRunner.js';
|
||||
|
||||
const backgroundRuns = new Map();
|
||||
|
||||
const BACKGROUND_JOBS = [
|
||||
{
|
||||
id: 'worker:host-group-sync',
|
||||
name: 'Dynamic Host Group Sync',
|
||||
category: 'Inventory',
|
||||
description: 'Reconciles rule-based Host Groups against the current Host Library.',
|
||||
run: async ({ signal } = {}) => syncDynamicHostGroups({ signal })
|
||||
},
|
||||
{
|
||||
id: 'worker:catalog-auto-check',
|
||||
name: 'Application Catalog Auto-Check',
|
||||
category: 'Applications',
|
||||
description: 'Checks opted-in application records for newer upstream versions.',
|
||||
run: async ({ signal } = {}) => runCatalogChecks({ signal })
|
||||
},
|
||||
{
|
||||
id: 'worker:intune-auto-promote',
|
||||
name: 'Intune Auto-Promotion',
|
||||
category: 'Intune',
|
||||
description: 'Promotes deployment rings when configured soak windows elapse.',
|
||||
run: async ({ signal } = {}) => runAutoPromotions({ signal })
|
||||
}
|
||||
];
|
||||
|
||||
export function listSystemJobs() {
|
||||
return {
|
||||
jobs: [...executionJobs(), ...backgroundJobs()],
|
||||
actions: listSystemJobActions(200),
|
||||
activeExecutionJobIds: activeExecutionJobIds()
|
||||
};
|
||||
}
|
||||
|
||||
export async function runSystemJob(targetId, actorUserId) {
|
||||
const definition = BACKGROUND_JOBS.find((job) => job.id === targetId);
|
||||
if (!definition) {
|
||||
const action = recordSystemJobAction({
|
||||
targetType: 'system',
|
||||
targetId,
|
||||
action: 'run-now',
|
||||
status: 'failed',
|
||||
message: 'System job not found.',
|
||||
actorUserId
|
||||
});
|
||||
return { ok: false, action, error: 'System job not found.' };
|
||||
}
|
||||
if (backgroundRuns.get(targetId)?.running) {
|
||||
const action = recordSystemJobAction({
|
||||
targetType: 'system',
|
||||
targetId,
|
||||
action: 'run-now',
|
||||
status: 'skipped',
|
||||
message: `${definition.name} is already running.`,
|
||||
actorUserId
|
||||
});
|
||||
return { ok: false, action, error: `${definition.name} is already running.` };
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
backgroundRuns.set(targetId, {
|
||||
running: true,
|
||||
cancelRequested: false,
|
||||
startedAt: now(),
|
||||
lastError: '',
|
||||
lastSummary: null,
|
||||
controller
|
||||
});
|
||||
const startAction = recordSystemJobAction({
|
||||
targetType: 'system',
|
||||
targetId,
|
||||
action: 'run-now',
|
||||
status: 'started',
|
||||
message: `${definition.name} started by operator.`,
|
||||
actorUserId
|
||||
});
|
||||
try {
|
||||
const summary = await definition.run({ signal: controller.signal });
|
||||
const canceled = Boolean(backgroundRuns.get(targetId)?.cancelRequested || controller.signal.aborted);
|
||||
backgroundRuns.set(targetId, {
|
||||
running: false,
|
||||
cancelRequested: false,
|
||||
startedAt: '',
|
||||
lastRunAt: now(),
|
||||
lastError: canceled ? 'Canceled by operator.' : '',
|
||||
lastSummary: summary
|
||||
});
|
||||
const action = recordSystemJobAction({
|
||||
targetType: 'system',
|
||||
targetId,
|
||||
action: 'run-now',
|
||||
status: canceled ? 'canceled' : 'success',
|
||||
message: canceled ? `${definition.name} canceled by operator.` : `${definition.name} completed.`,
|
||||
actorUserId,
|
||||
metadata: { summary, startActionId: startAction.id }
|
||||
});
|
||||
return { ok: !canceled, action, summary, canceled };
|
||||
} catch (error) {
|
||||
const canceled = Boolean(backgroundRuns.get(targetId)?.cancelRequested || controller.signal.aborted);
|
||||
backgroundRuns.set(targetId, {
|
||||
running: false,
|
||||
cancelRequested: false,
|
||||
startedAt: '',
|
||||
lastRunAt: now(),
|
||||
lastError: canceled ? 'Canceled by operator.' : error.message,
|
||||
lastSummary: null
|
||||
});
|
||||
const action = recordSystemJobAction({
|
||||
targetType: 'system',
|
||||
targetId,
|
||||
action: 'run-now',
|
||||
status: canceled ? 'canceled' : 'failed',
|
||||
message: canceled ? `${definition.name} canceled by operator.` : error.message,
|
||||
actorUserId,
|
||||
metadata: { startActionId: startAction.id }
|
||||
});
|
||||
return { ok: false, action, error: canceled ? 'Canceled by operator.' : error.message, canceled };
|
||||
}
|
||||
}
|
||||
|
||||
export function cancelSystemJob(targetId, actorUserId, reason = '') {
|
||||
if (targetId.startsWith('worker:')) {
|
||||
const definition = BACKGROUND_JOBS.find((job) => job.id === targetId);
|
||||
const state = backgroundRuns.get(targetId);
|
||||
if (!definition || !state?.running) {
|
||||
const action = recordSystemJobAction({
|
||||
targetType: 'system',
|
||||
targetId,
|
||||
action: 'cancel',
|
||||
status: 'failed',
|
||||
message: definition ? `${definition.name} is not running.` : 'System job not found.',
|
||||
actorUserId,
|
||||
metadata: { reason }
|
||||
});
|
||||
return { ok: false, action, error: action.message };
|
||||
}
|
||||
state.cancelRequested = true;
|
||||
state.controller?.abort?.(new Error(reason || 'Canceled by operator.'));
|
||||
backgroundRuns.set(targetId, state);
|
||||
const action = recordSystemJobAction({
|
||||
targetType: 'system',
|
||||
targetId,
|
||||
action: 'cancel',
|
||||
status: 'success',
|
||||
message: `Cancel requested for ${definition.name}.`,
|
||||
actorUserId,
|
||||
metadata: { reason }
|
||||
});
|
||||
return { ok: true, action, result: { canceled: true, targetType: 'background' } };
|
||||
}
|
||||
|
||||
if (!targetId.startsWith('job_')) {
|
||||
const action = recordSystemJobAction({
|
||||
targetType: 'system',
|
||||
targetId,
|
||||
action: 'cancel',
|
||||
status: 'failed',
|
||||
message: 'Only running script execution jobs can be canceled.',
|
||||
actorUserId,
|
||||
metadata: { reason }
|
||||
});
|
||||
return { ok: false, action, error: 'Only running script execution jobs can be canceled.' };
|
||||
}
|
||||
const result = cancelExecutionJob(targetId, { actorUserId, reason });
|
||||
const status = result.canceled ? 'success' : 'failed';
|
||||
const message = result.canceled
|
||||
? `Cancel requested; signaled ${result.signaledProcesses || 0} running process(es).`
|
||||
: result.reason;
|
||||
const action = recordSystemJobAction({
|
||||
jobId: targetId,
|
||||
targetType: 'execution',
|
||||
targetId,
|
||||
action: 'cancel',
|
||||
status,
|
||||
message,
|
||||
actorUserId,
|
||||
metadata: { reason, result }
|
||||
});
|
||||
if (result.canceled) appendJobLog(targetId, 'system', `Job management action logged: cancel (${action.id}).`, null);
|
||||
return { ok: result.canceled, action, result, error: result.canceled ? null : result.reason };
|
||||
}
|
||||
|
||||
function executionJobs() {
|
||||
const rows = db.prepare(`
|
||||
SELECT j.*, r.name AS runplan_name, s.name AS script_name,
|
||||
COUNT(jh.id) AS host_count,
|
||||
SUM(CASE WHEN jh.status = 'running' THEN 1 ELSE 0 END) AS running_hosts,
|
||||
SUM(CASE WHEN jh.status = 'queued' THEN 1 ELSE 0 END) AS queued_hosts
|
||||
FROM jobs j
|
||||
LEFT JOIN runplans r ON r.id = j.runplan_id
|
||||
LEFT JOIN scripts s ON s.id = j.script_id
|
||||
LEFT JOIN job_hosts jh ON jh.job_id = j.id
|
||||
GROUP BY j.id
|
||||
ORDER BY j.created_at DESC
|
||||
LIMIT 200
|
||||
`).all();
|
||||
return rows.map((row) => {
|
||||
const job = camelJob(row);
|
||||
const running = ['queued', 'running'].includes(job.status);
|
||||
return {
|
||||
id: job.id,
|
||||
type: 'execution',
|
||||
category: 'Script Execution',
|
||||
name: job.runplanName || job.scriptName || job.id,
|
||||
description: job.runplanName ? `RunPlan ${job.runplanName}` : `Script test ${job.scriptName || job.id}`,
|
||||
status: job.status,
|
||||
canCancel: running,
|
||||
canRunNow: false,
|
||||
startedAt: job.startedAt || '',
|
||||
endedAt: job.endedAt || '',
|
||||
createdAt: job.createdAt,
|
||||
hostCount: row.host_count || 0,
|
||||
runningHosts: row.running_hosts || 0,
|
||||
queuedHosts: row.queued_hosts || 0,
|
||||
metadata: {
|
||||
runplanId: job.runplanId,
|
||||
scriptId: job.scriptId,
|
||||
triggeredBy: job.triggeredBy
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function backgroundJobs() {
|
||||
return BACKGROUND_JOBS.map((job) => {
|
||||
const state = backgroundRuns.get(job.id) || {};
|
||||
return {
|
||||
id: job.id,
|
||||
type: 'background',
|
||||
category: job.category,
|
||||
name: job.name,
|
||||
description: job.description,
|
||||
status: state.cancelRequested ? 'canceling' : state.running ? 'running' : 'idle',
|
||||
canCancel: Boolean(state.running),
|
||||
canRunNow: !state.running,
|
||||
startedAt: state.startedAt || '',
|
||||
endedAt: '',
|
||||
createdAt: '',
|
||||
hostCount: 0,
|
||||
runningHosts: 0,
|
||||
queuedHosts: 0,
|
||||
metadata: {
|
||||
lastRunAt: state.lastRunAt || '',
|
||||
lastError: state.lastError || '',
|
||||
lastSummary: state.lastSummary || null
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user