diff --git a/striker-ui-api/src/lib/consts/SERVER_PATHS.ts b/striker-ui-api/src/lib/consts/SERVER_PATHS.ts index 402988a1..f8cc8a2c 100644 --- a/striker-ui-api/src/lib/consts/SERVER_PATHS.ts +++ b/striker-ui-api/src/lib/consts/SERVER_PATHS.ts @@ -11,6 +11,7 @@ const EMPTY_SERVER_PATHS: ServerPath = { bin: { date: {}, mkfifo: {}, + psql: {}, rm: {}, sed: {}, }, @@ -25,6 +26,7 @@ const EMPTY_SERVER_PATHS: ServerPath = { 'anvil-update-system': {}, 'striker-initialize-host': {}, 'striker-manage-install-target': {}, + 'striker-manage-peers': {}, 'striker-parse-os-list': {}, }, }, diff --git a/striker-ui-api/src/lib/request_handlers/host/createHostConnection.ts b/striker-ui-api/src/lib/request_handlers/host/createHostConnection.ts new file mode 100644 index 00000000..c688dc9c --- /dev/null +++ b/striker-ui-api/src/lib/request_handlers/host/createHostConnection.ts @@ -0,0 +1,190 @@ +import { RequestHandler } from 'express'; + +import SERVER_PATHS from '../../consts/SERVER_PATHS'; + +import { + getAnvilData, + getLocalHostUUID, + getPeerData, + job, + sub, +} from '../../accessModule'; +import { sanitize } from '../../sanitize'; +import { rm, stderr, stdoutVar } from '../../shell'; + +export const createHostConnection: RequestHandler< + unknown, + undefined, + CreateHostConnectionRequestBody +> = (request, response) => { + const { + body: { + dbName = 'anvil', + ipAddress, + isPing = false, + password, + port = 5432, + sshPort = 22, + user = 'admin', + }, + } = request; + + const commonDBName = sanitize(dbName, 'string'); + const commonIsPing = sanitize(isPing, 'boolean'); + const commonPassword = sanitize(password, 'string'); + const commonDBPort = sanitize(port, 'number'); + const commonDBUser = sanitize(user, 'string'); + const peerIPAddress = sanitize(ipAddress, 'string'); + const peerSSHPort = sanitize(sshPort, 'number'); + + const commonPing = commonIsPing ? 1 : 0; + + let localDBPort: number; + let localIPAddress: string; + let isPeerReachable = false; + let isPeerDBReachable = false; + let peerHostUUID: string; + + try { + ({ hostUUID: peerHostUUID, isConnected: isPeerReachable } = getPeerData( + peerIPAddress, + { password: commonPassword, port: peerSSHPort }, + )); + } catch (subError) { + stderr(`Failed to get peer data; CAUSE: ${subError}`); + + response.status(500).send(); + + return; + } + + stdoutVar({ peerHostUUID, isPeerReachable }); + + if (!isPeerReachable) { + stderr( + `Cannot connect to peer; please verify credentials and SSH keys validity.`, + ); + + response.status(400).send(); + + return; + } + + try { + localIPAddress = sub('find_matching_ip', { + subModuleName: 'System', + subParams: { host: peerIPAddress }, + }).stdout; + } catch (subError) { + stderr(`Failed to get matching IP address; CAUSE: ${subError}`); + + response.status(500).send(); + + return; + } + + stdoutVar({ localIPAddress }); + + const pgpassFilePath = '/tmp/.pgpass'; + const pgpassFileBody = `${peerIPAddress}:${commonDBPort}:${commonDBName}:${commonDBUser}:${commonPassword.replace( + /:/g, + '\\:', + )}`; + + stdoutVar({ pgpassFilePath, pgpassFileBody }); + + try { + sub('write_file', { + subModuleName: 'Storage', + subParams: { + body: pgpassFileBody, + file: pgpassFilePath, + mode: '0600', + overwrite: 1, + secure: 1, + }, + }); + } catch (subError) { + stderr(`Failed to write ${pgpassFilePath}; CAUSE: ${subError}`); + + response.status(500).send(); + + return; + } + + try { + const [rawIsPeerDBReachable] = sub('call', { + subModuleName: 'System', + subParams: { + shell_call: `PGPASSFILE="${pgpassFilePath}" ${SERVER_PATHS.usr.bin.psql.self} --host ${peerIPAddress} --port ${commonDBPort} --dbname ${commonDBName} --username ${commonDBUser} --no-password --tuples-only --no-align --command "SELECT 1"`, + }, + }).stdout as [output: string, returnCode: number]; + + isPeerDBReachable = rawIsPeerDBReachable === '1'; + } catch (subError) { + stderr(`Failed to test connection to peer database; CAUSE: ${subError}`); + } + + try { + rm(pgpassFilePath); + } catch (fsError) { + stderr(`Failed to remove ${pgpassFilePath}; CAUSE: ${fsError}`); + + response.status(500).send(); + + return; + } + + stdoutVar({ isPeerDBReachable }); + + if (!isPeerDBReachable) { + stderr( + `Cannot connect to peer database; please verify database credentials.`, + ); + + response.status(400).send(); + + return; + } + + const localHostUUID = getLocalHostUUID(); + + try { + const { + database: { + [localHostUUID]: { port: rawLocalDBPort }, + }, + } = getAnvilData({ database: true }) as { database: DatabaseHash }; + + localDBPort = sanitize(rawLocalDBPort, 'number'); + } catch (subError) { + stderr(`Failed to get local database data from hash; CAUSE: ${subError}`); + + response.status(500).send(); + + return; + } + + const jobCommand = `${SERVER_PATHS.usr.sbin['striker-manage-peers'].self} --add --host-uuid ${peerHostUUID} --host ${peerIPAddress} --port ${commonDBPort} --ping ${commonPing}`; + const peerJobCommand = `${SERVER_PATHS.usr.sbin['striker-manage-peers'].self} --add --host-uuid ${localHostUUID} --host ${localIPAddress} --port ${localDBPort} --ping ${commonPing}`; + + try { + job({ + file: __filename, + job_command: jobCommand, + job_data: `password=${commonPassword} +peer_job_command=${peerJobCommand}`, + job_description: 'job_0012', + job_name: 'striker-peer::add', + job_title: 'job_0011', + }); + } catch (subError) { + stderr(`Failed to add peer ${peerHostUUID}; CAUSE: ${subError}`); + + response.status(500).send(); + + return; + } + + response.status(201).send(); +}; diff --git a/striker-ui-api/src/lib/request_handlers/host/deleteHostConnection.ts b/striker-ui-api/src/lib/request_handlers/host/deleteHostConnection.ts new file mode 100644 index 00000000..3f23f7f4 --- /dev/null +++ b/striker-ui-api/src/lib/request_handlers/host/deleteHostConnection.ts @@ -0,0 +1,42 @@ +import { RequestHandler } from 'express'; + +import SERVER_PATHS from '../../consts/SERVER_PATHS'; + +import { job } from '../../accessModule'; +import { toHostUUID } from '../../convertHostUUID'; +import { stderr } from '../../shell'; + +export const deleteHostConnection: RequestHandler< + unknown, + undefined, + DeleteHostConnectionRequestBody +> = (request, response) => { + const { body } = request; + const hostUUIDs = Object.keys(body); + + hostUUIDs.forEach((key) => { + const hostUUID = toHostUUID(key); + const peerHostUUIDs = body[key]; + + peerHostUUIDs.forEach((peerHostUUID) => { + try { + job({ + file: __filename, + job_command: `${SERVER_PATHS.usr.sbin['striker-manage-peers'].self} --remove --host-uuid ${peerHostUUID}`, + job_description: 'job_0014', + job_host_uuid: hostUUID, + job_name: 'striker-peer::delete', + job_title: 'job_0013', + }); + } catch (subError) { + stderr(`Failed to delete peer ${peerHostUUID}; CAUSE: ${subError}`); + + response.status(500).send(); + + return; + } + }); + }); + + response.status(204).send(); +}; diff --git a/striker-ui-api/src/lib/request_handlers/host/getHostConnection.ts b/striker-ui-api/src/lib/request_handlers/host/getHostConnection.ts index eaf29ea2..8671ac32 100644 --- a/striker-ui-api/src/lib/request_handlers/host/getHostConnection.ts +++ b/striker-ui-api/src/lib/request_handlers/host/getHostConnection.ts @@ -42,16 +42,7 @@ export const getHostConnection = buildGetRequestHandler( (request, buildQueryOptions) => { const { hostUUIDs: rawHostUUIDs } = request.query; - let rawDatabaseData: { - [hostUUID: string]: { - host: string; - name: string; - password: string; - ping: string; - port: string; - user: string; - }; - }; + let rawDatabaseData: DatabaseHash; const hostUUIDField = 'ip_add.ip_address_host_uuid'; const localHostUUID: string = getLocalHostUUID(); diff --git a/striker-ui-api/src/lib/request_handlers/host/index.ts b/striker-ui-api/src/lib/request_handlers/host/index.ts index af8ddab8..df94e1bb 100644 --- a/striker-ui-api/src/lib/request_handlers/host/index.ts +++ b/striker-ui-api/src/lib/request_handlers/host/index.ts @@ -1,4 +1,6 @@ export * from './createHost'; +export * from './createHostConnection'; +export * from './deleteHostConnection'; export * from './getHost'; export * from './getHostConnection'; export * from './getHostDetail'; diff --git a/striker-ui-api/src/lib/request_handlers/host/prepareHost.ts b/striker-ui-api/src/lib/request_handlers/host/prepareHost.ts index 13702807..6f9d16bf 100644 --- a/striker-ui-api/src/lib/request_handlers/host/prepareHost.ts +++ b/striker-ui-api/src/lib/request_handlers/host/prepareHost.ts @@ -11,7 +11,7 @@ import SERVER_PATHS from '../../consts/SERVER_PATHS'; import { job, variable } from '../../accessModule'; import { sanitize } from '../../sanitize'; -import { stderr, stdout } from '../../shell'; +import { stderr } from '../../shell'; export const prepareHost: RequestHandler< unknown, diff --git a/striker-ui-api/src/lib/request_handlers/ssh-key/deleteSSHKeyConflict.ts b/striker-ui-api/src/lib/request_handlers/ssh-key/deleteSSHKeyConflict.ts index 94e76458..c4144efb 100644 --- a/striker-ui-api/src/lib/request_handlers/ssh-key/deleteSSHKeyConflict.ts +++ b/striker-ui-api/src/lib/request_handlers/ssh-key/deleteSSHKeyConflict.ts @@ -3,6 +3,7 @@ import { RequestHandler } from 'express'; import SERVER_PATHS from '../../consts/SERVER_PATHS'; import { job } from '../../accessModule'; +import { toHostUUID } from '../../convertHostUUID'; import { stderr } from '../../shell'; export const deleteSSHKeyConflict: RequestHandler< @@ -13,8 +14,9 @@ export const deleteSSHKeyConflict: RequestHandler< const { body } = request; const hostUUIDs = Object.keys(body); - hostUUIDs.forEach((hostUUID) => { - const stateUUIDs = body[hostUUID]; + hostUUIDs.forEach((key) => { + const hostUUID = toHostUUID(key); + const stateUUIDs = body[key]; try { job({ diff --git a/striker-ui-api/src/routes/host.ts b/striker-ui-api/src/routes/host.ts index 644b78d2..f0e1d39e 100644 --- a/striker-ui-api/src/routes/host.ts +++ b/striker-ui-api/src/routes/host.ts @@ -2,6 +2,8 @@ import express from 'express'; import { createHost, + createHostConnection, + deleteHostConnection, getHost, getHostConnection, getHostDetail, @@ -9,14 +11,18 @@ import { updateHost, } from '../lib/request_handlers/host'; +const CONNECTION_PATH = '/connection'; + const router = express.Router(); router .get('/', getHost) - .get('/connection', getHostConnection) + .get(CONNECTION_PATH, getHostConnection) .get('/:hostUUID', getHostDetail) .post('/', createHost) + .post(CONNECTION_PATH, createHostConnection) .put('/prepare', prepareHost) - .put('/:hostUUID', updateHost); + .put('/:hostUUID', updateHost) + .delete(CONNECTION_PATH, deleteHostConnection); export default router; diff --git a/striker-ui-api/src/types/CreateHostConnectionRequestBody.d.ts b/striker-ui-api/src/types/CreateHostConnectionRequestBody.d.ts new file mode 100644 index 00000000..ce50d3a2 --- /dev/null +++ b/striker-ui-api/src/types/CreateHostConnectionRequestBody.d.ts @@ -0,0 +1,11 @@ +type CreateHostConnectionRequestBody = { + dbName?: string; + ipAddress: string; + isPing?: boolean; + // Host password; same as database password. + password: string; + port?: number; + sshPort?: number; + // database user. + user?: string; +}; diff --git a/striker-ui-api/src/types/DeleteHostConnectionRequestBody.d.ts b/striker-ui-api/src/types/DeleteHostConnectionRequestBody.d.ts new file mode 100644 index 00000000..cea743b6 --- /dev/null +++ b/striker-ui-api/src/types/DeleteHostConnectionRequestBody.d.ts @@ -0,0 +1,3 @@ +type DeleteHostConnectionRequestBody = { + [hostUUID: string]: string[]; +};