This commit is contained in:
2026-06-25 14:43:13 -05:00
parent 6f77fac58e
commit aaa6a9ee26
25 changed files with 1542 additions and 67 deletions

View File

@@ -8,6 +8,8 @@ import { logger } from './logger.js';
import { listExecutableAssets } from '../models/assetModel.js';
import { listExecutableCustomVariables } from '../models/variableModel.js';
import { getBooleanSetting, getStringSetting } from '../models/settingsModel.js';
import { getVCenterConnectionSystem } from '../models/vcenterConnectionModel.js';
import { getVCenterVmPowerState } from './vcenterService.js';
import { path } from '../utils/pathUtils.js';
// The execution kill-switch can be set at boot via ALLOW_SCRIPT_EXECUTION and
@@ -45,12 +47,18 @@ export function executeRunPlan(runplanId, userId) {
const hosts = db.prepare(`
SELECT h.*, c.name AS credential_name, c.kind AS credential_kind, c.username AS credential_username,
c.secret_cipher, c.secret_iv, c.secret_tag
FROM runplan_hosts rh
JOIN hosts h ON h.id = rh.host_id
FROM hosts h
LEFT JOIN credentials c ON c.id = h.credential_id
WHERE rh.runplan_id = ?
WHERE h.id IN (
SELECT host_id FROM runplan_hosts WHERE runplan_id = ?
UNION
SELECT hgm.host_id
FROM runplan_host_groups rhg
JOIN host_group_members hgm ON hgm.host_group_id = rhg.host_group_id
WHERE rhg.runplan_id = ?
)
ORDER BY h.name
`).all(runplanId);
`).all(runplanId, runplanId);
if (hosts.length === 0) throw new Error('RunPlan has no hosts');
const jobId = id('job');
@@ -95,7 +103,31 @@ export function executeScriptTest(scriptId, payload, userId) {
`).get(scriptId, userId, userId);
if (!script) throw new Error('Script not found');
const host = db.prepare(`
const hosts = payload.hostGroupId ? db.prepare(`
SELECT h.*, c.name AS credential_name, c.kind AS credential_kind, c.username AS credential_username,
c.secret_cipher, c.secret_iv, c.secret_tag
FROM host_group_members hgm
JOIN host_groups hg ON hg.id = hgm.host_group_id
AND (
hg.visibility = 'shared'
OR hg.owner_user_id = ?
OR hg.group_id IN (SELECT group_id FROM user_groups WHERE user_id = ?)
)
JOIN hosts h ON h.id = hgm.host_id
AND (
h.visibility = 'shared'
OR h.owner_user_id = ?
OR h.group_id IN (SELECT group_id FROM user_groups WHERE user_id = ?)
)
JOIN credentials c ON c.id = ?
AND (
c.visibility = 'shared'
OR c.owner_user_id = ?
OR c.group_id IN (SELECT group_id FROM user_groups WHERE user_id = ?)
)
WHERE hgm.host_group_id = ?
ORDER BY h.name
`).all(userId, userId, userId, userId, payload.credentialId, userId, userId, payload.hostGroupId) : [db.prepare(`
SELECT h.*, c.name AS credential_name, c.kind AS credential_kind, c.username AS credential_username,
c.secret_cipher, c.secret_iv, c.secret_tag
FROM hosts h
@@ -110,8 +142,8 @@ export function executeScriptTest(scriptId, payload, userId) {
OR h.owner_user_id = ?
OR h.group_id IN (SELECT group_id FROM user_groups WHERE user_id = ?)
)
`).get(payload.credentialId, userId, userId, payload.hostId, userId, userId);
if (!host) throw new Error('Visible host and credential are required');
`).get(payload.credentialId, userId, userId, payload.hostId, userId, userId)].filter(Boolean);
if (!hosts.length) throw new Error('Visible host or host group members and credential are required');
const jobId = id('job');
db.prepare(`
@@ -121,7 +153,8 @@ export function executeScriptTest(scriptId, payload, userId) {
db.prepare(`
INSERT INTO job_hosts (id, job_id, host_id, status, started_at)
VALUES (?, ?, ?, 'queued', NULL)
`).run(id('jhost'), jobId, host.id);
`);
for (const host of hosts) insertJobHost.run(id('jhost'), jobId, host.id);
const executionContext = {
jobId,
@@ -134,10 +167,10 @@ export function executeScriptTest(scriptId, payload, userId) {
assets: listExecutableAssets(userId)
};
appendLog(jobId, host.id, 'system', `Test run requested for ${script.name} using credential ${host.credential_name}`);
void runJob(jobId, script, [host], false, executionContext).catch((error) => {
for (const host of hosts) appendLog(jobId, host.id, 'system', `Test run requested for ${script.name} using credential ${host.credential_name}`);
void runJob(jobId, script, hosts, false, executionContext).catch((error) => {
logger.error('script test runner crashed', { jobId, error });
appendLog(jobId, host.id, 'stderr', `Script test runner crashed: ${error.message}`);
for (const host of hosts) appendLog(jobId, host.id, 'stderr', `Script test runner crashed: ${error.message}`);
db.prepare('UPDATE jobs SET status = ?, ended_at = ? WHERE id = ?').run('failed', now(), jobId);
});
@@ -168,6 +201,8 @@ async function runHost(jobId, script, host, executionContext) {
return;
}
if (!(await vCenterPowerPreflight(jobId, host))) return;
const file = path.join(os.tmpdir(), `poshmanager-${jobId}-${host.id}.ps1`);
try {
@@ -183,6 +218,35 @@ async function runHost(jobId, script, host, executionContext) {
}
}
async function vCenterPowerPreflight(jobId, host) {
if ((host.source_type || 'manual') !== 'vmware') return true;
if (!host.source_connection_id || !host.source_ref) {
appendLog(jobId, host.id, 'system', 'Host is marked as VMware-sourced, but no vCenter connection or VM reference is stored. Continuing without a power-state gate.');
return true;
}
const connection = getVCenterConnectionSystem(host.source_connection_id, true);
if (!connection) {
appendLog(jobId, host.id, 'system', `Host is VMware-sourced, but vCenter connection ${host.source_connection_id} no longer exists. Continuing without a power-state gate.`);
return true;
}
try {
const result = await getVCenterVmPowerState(connection, host.source_ref);
if (!result.checked) {
appendLog(jobId, host.id, 'system', `vCenter power-state check skipped: ${result.reason || 'not enough source metadata'}`);
return true;
}
appendLog(jobId, host.id, 'system', `vCenter power state for VM ${host.source_ref}: ${result.state || 'unknown'} (${result.apiProfile || 'vCenter API'})`);
if (result.state && result.state !== 'POWERED_ON') {
appendLog(jobId, host.id, 'system', `Skipping ${host.name}; VMware VM is not powered on.`);
finishHost(jobId, host.id, 'skipped', 0);
return false;
}
} catch (err) {
appendLog(jobId, host.id, 'system', `Unable to verify vCenter power state before execution: ${err.message}. Continuing without a power-state gate.`);
}
return true;
}
function buildWrapper(content, host, executionContext = {}) {
// Build PowerShell wrappers without shell interpolation; host values are quoted as PS literals.
const runtimeVariables = runtimeVariableValues(host, executionContext);