diff --git a/striker-ui-api/src/lib/consts/SERVER_PATHS.ts b/striker-ui-api/src/lib/consts/SERVER_PATHS.ts index 2155571b..6fad8458 100644 --- a/striker-ui-api/src/lib/consts/SERVER_PATHS.ts +++ b/striker-ui-api/src/lib/consts/SERVER_PATHS.ts @@ -30,6 +30,7 @@ const EMPTY_SERVER_PATHS: ServerPath = { 'anvil-manage-keys': {}, 'anvil-manage-power': {}, 'anvil-provision-server': {}, + 'anvil-safe-start': {}, 'anvil-safe-stop': {}, 'anvil-sync-shared': {}, 'anvil-update-system': {}, diff --git a/striker-ui-api/src/lib/request_handlers/command/buildMembershipHandler.ts b/striker-ui-api/src/lib/request_handlers/command/buildMembershipHandler.ts new file mode 100644 index 00000000..f3fc0dba --- /dev/null +++ b/striker-ui-api/src/lib/request_handlers/command/buildMembershipHandler.ts @@ -0,0 +1,123 @@ +import assert from 'assert'; +import { RequestHandler } from 'express'; + +import { REP_UUID, SERVER_PATHS } from '../../consts'; + +import { job, query } from '../../accessModule'; +import { sanitize } from '../../sanitize'; +import { stderr } from '../../shell'; + +const MAP_TO_MEMBERSHIP_JOB_PARAMS_BUILDER: Record< + MembershipTask, + BuildMembershipJobParamsFunction +> = { + join: async (uuid, { isActiveMember } = {}) => { + // Host is already a cluster member + if (isActiveMember) return undefined; + + const rows: [[number]] = await query( + `SELECT + CASE + WHEN host_status = 'online' + THEN CAST('1' AS BOOLEAN) + ELSE CAST('0' AS BOOLEAN) + END + FROM hosts WHERE host_uuid = '${uuid}';`, + ); + + assert.ok(rows.length, 'No entry found'); + + const [[isOnline]] = rows; + + return isOnline + ? { + job_command: SERVER_PATHS.usr.sbin['anvil-safe-start'].self, + job_description: 'job_0337', + job_host_uuid: uuid, + job_name: 'set_membership::join', + job_title: 'job_0336', + } + : undefined; + }, + leave: async (uuid, { isActiveMember } = {}) => + isActiveMember + ? { + job_command: SERVER_PATHS.usr.sbin['anvil-safe-stop'].self, + job_description: 'job_0339', + job_host_uuid: uuid, + job_name: 'set_membership::leave', + job_title: 'job_0338', + } + : undefined, +}; + +export const buildMembershipHandler: ( + task: MembershipTask, +) => RequestHandler<{ uuid: string }> = (task) => async (request, response) => { + const { + params: { uuid }, + } = request; + + const hostUuid = sanitize(uuid, 'string', { modifierType: 'sql' }); + + try { + assert( + REP_UUID.test(hostUuid), + `Param UUID must be a valid UUIDv4; got: [${hostUuid}]`, + ); + } catch (error) { + stderr( + `Failed to assert value when changing host membership; CAUSE: ${error}`, + ); + + return response.status(500).send(); + } + + let rows: [ + [ + hostInCcm: NumberBoolean, + hostCrmdMember: NumberBoolean, + hostClusterMember: NumberBoolean, + ], + ]; + + try { + rows = await query( + `SELECT + scan_cluster_node_in_ccm, + scan_cluster_node_crmd_member, + scan_cluster_node_cluster_member + FROM scan_cluster_nodes + WHERE scan_cluster_node_host_uuid = '${hostUuid}';`, + ); + + assert.ok(rows.length, `No entry found`); + } catch (error) { + stderr(`Failed to get cluster status of host ${hostUuid}; CAUSE: ${error}`); + + return response.status(500).send(); + } + + const isActiveMember = rows[0].every((stage) => Boolean(stage)); + + try { + const restParams = await MAP_TO_MEMBERSHIP_JOB_PARAMS_BUILDER[task]( + hostUuid, + { + isActiveMember, + }, + ); + + if (restParams) { + await job({ file: __filename, ...restParams }); + } + } catch (error) { + stderr( + `Failed to initiate ${task} cluster for host ${hostUuid}; CAUSE: ${error}`, + ); + + return response.status(500).send(); + } + + return response.status(204).send(); +}; diff --git a/striker-ui-api/src/lib/request_handlers/command/buildPowerHandler.ts b/striker-ui-api/src/lib/request_handlers/command/buildPowerHandler.ts index 486e85b8..7dde98fe 100644 --- a/striker-ui-api/src/lib/request_handlers/command/buildPowerHandler.ts +++ b/striker-ui-api/src/lib/request_handlers/command/buildPowerHandler.ts @@ -12,21 +12,21 @@ import { stderr } from '../../shell'; * * poweroff, reboot targets the striker this express app operates on * * start, stop targets subnode or DR host */ -const MANAGE_HOST_POWER_JOB_PARAMS: Record< +const MAP_TO_POWER_JOB_PARAMS_BUILDER: Record< PowerTask, BuildPowerJobParamsFunction > = { poweroff: () => ({ job_command: `${SERVER_PATHS.usr.sbin['anvil-manage-power'].self} --poweroff -y`, + job_description: 'job_0008', job_name: 'poweroff::system', job_title: 'job_0010', - job_description: 'job_0008', }), reboot: () => ({ job_command: `${SERVER_PATHS.usr.sbin['anvil-manage-power'].self} --reboot -y`, + job_description: 'job_0006', job_name: 'reboot::system', job_title: 'job_0009', - job_description: 'job_0006', }), start: ({ uuid } = {}) => ({ job_command: `${SERVER_PATHS.usr.sbin['striker-boot-machine'].self} --host-uuid '${uuid}'`, @@ -49,13 +49,13 @@ const queuePowerJob = async ( task: PowerTask, options?: BuildPowerJobParamsOptions, ) => { - const subParams: JobParams = { + const params: JobParams = { file: __filename, - ...MANAGE_HOST_POWER_JOB_PARAMS[task](options), + ...MAP_TO_POWER_JOB_PARAMS_BUILDER[task](options), }; - return await job(subParams); + return await job(params); }; export const buildPowerHandler: ( diff --git a/striker-ui-api/src/lib/request_handlers/command/index.ts b/striker-ui-api/src/lib/request_handlers/command/index.ts index 978368b1..92feabe7 100644 --- a/striker-ui-api/src/lib/request_handlers/command/index.ts +++ b/striker-ui-api/src/lib/request_handlers/command/index.ts @@ -1,4 +1,6 @@ export * from './getHostSSH'; +export * from './joinAn'; +export * from './leaveAn'; export * from './poweroffStriker'; export * from './rebootStriker'; export * from './runManifest'; diff --git a/striker-ui-api/src/lib/request_handlers/command/joinAn.ts b/striker-ui-api/src/lib/request_handlers/command/joinAn.ts new file mode 100644 index 00000000..02acd8cb --- /dev/null +++ b/striker-ui-api/src/lib/request_handlers/command/joinAn.ts @@ -0,0 +1,3 @@ +import { buildMembershipHandler } from './buildMembershipHandler'; + +export const joinAn = buildMembershipHandler('join'); diff --git a/striker-ui-api/src/lib/request_handlers/command/leaveAn.ts b/striker-ui-api/src/lib/request_handlers/command/leaveAn.ts new file mode 100644 index 00000000..5bd47c88 --- /dev/null +++ b/striker-ui-api/src/lib/request_handlers/command/leaveAn.ts @@ -0,0 +1,3 @@ +import { buildMembershipHandler } from './buildMembershipHandler'; + +export const leaveAn = buildMembershipHandler('leave'); diff --git a/striker-ui-api/src/routes/command.ts b/striker-ui-api/src/routes/command.ts index 9eab5515..0aac6ecf 100644 --- a/striker-ui-api/src/routes/command.ts +++ b/striker-ui-api/src/routes/command.ts @@ -2,6 +2,8 @@ import express from 'express'; import { getHostSSH, + joinAn, + leaveAn, poweroffStriker, rebootStriker, runManifest, @@ -16,6 +18,8 @@ const router = express.Router(); router .put('/inquire-host', getHostSSH) + .put('/join-an/:uuid', joinAn) + .put('/leave-an/:uuid', leaveAn) .put('/poweroff-host', poweroffStriker) .put('/reboot-host', rebootStriker) .put('/run-manifest/:manifestUuid', runManifest) diff --git a/striker-ui-api/src/types/BuildMembershipHandlerFunction.d.ts b/striker-ui-api/src/types/BuildMembershipHandlerFunction.d.ts new file mode 100644 index 00000000..7b7394e7 --- /dev/null +++ b/striker-ui-api/src/types/BuildMembershipHandlerFunction.d.ts @@ -0,0 +1,12 @@ +type MembershipTask = 'join' | 'leave'; + +type MembershipJobParams = Omit; + +type BuildMembershipJobParamsOptions = { + isActiveMember?: boolean; +}; + +type BuildMembershipJobParamsFunction = ( + uuid: string, + options?: BuildMembershipJobParamsOptions, +) => Promise; diff --git a/striker-ui-api/src/types/BuildPowerJobParamsFunction.d.ts b/striker-ui-api/src/types/BuildPowerHandlerFunction.d.ts similarity index 72% rename from striker-ui-api/src/types/BuildPowerJobParamsFunction.d.ts rename to striker-ui-api/src/types/BuildPowerHandlerFunction.d.ts index 8bb792ac..65e2cc29 100644 --- a/striker-ui-api/src/types/BuildPowerJobParamsFunction.d.ts +++ b/striker-ui-api/src/types/BuildPowerHandlerFunction.d.ts @@ -1,9 +1,6 @@ type PowerTask = 'poweroff' | 'reboot' | 'start' | 'stop'; -type PowerJobParams = Omit< - JobParams, - 'file' | 'line' | 'job_data' | 'job_progress' ->; +type PowerJobParams = Omit; type BuildPowerJobParamsOptions = { isStopServers?: boolean;