import { Awaitable, AwareOfContainerLifecycle, ErrorWithContext, Filesystem, infer, objectToKeyValue, UniversalPath, uuid4, } from '@extollo/lib' import {CommandPalette, CommandPaletteCommands, OwnershipSpec, shellCommand, ShellCommand} from './types' import {ExecutionResult} from './ExecutionResult' import {CommandError} from './errors' import {SystemMetrics} from './SystemMetrics' export class StandardCommandPalette implements CommandPalette { changeDirectory = shellCommand(`cd "%%PATH%%"`) cpuPercentage = shellCommand(`grep 'cpu ' /proc/stat | awk '{usage=($2+$4)*100/($2+$4+$5)} END {print usage}'`) fileDirectoryDelete = shellCommand(`rm -rf "%%RESOURCE%%"`) fileDirectoryOwnershipFetch = shellCommand(`stat -c "%U:%G" "%%PATH%%"`) fileDirectoryOwnershipSetFlat = shellCommand(`chown %%OWNERS%% "%%PATH%%"`) fileDirectoryOwnershipSetRecursive = shellCommand(`chown -R %%OWNERS%% "%%PATH%%"`) fileDirectoryPermissionFetch = shellCommand(`stat -c %a "%%PATH%%"`) fileDirectoryPermissionSetFlat = shellCommand(`chmod %%LEVEL%% "%%PATH%%"`) fileDirectoryPermissionSetRecursive = shellCommand(`chmod -R %%LEVEL%% "%%PATH%%"`) listMountPoints = shellCommand(`df -hl | grep '/' | awk '{print $6}`) mountPointPercentage = shellCommand(`df -hl | grep -w '%%MOUNTPOINT%%$' | awk '{print $5}'`) ramPercentage = shellCommand(`free | grep Mem | awk '{print $3/$2 * 100.0}'`) reboot = shellCommand(`reboot`) resolvePath = shellCommand(`readlink -f "%%PATH%%"`) tempFile = shellCommand(`mktemp`) tempPath = shellCommand(`mktemp -d`) echo = shellCommand(`echo "%%OUTPUT%%"`) public format(command: keyof CommandPaletteCommands, substitutions: Record): ShellCommand { let formatted = `${this[command]}` objectToKeyValue(substitutions) .map(item => formatted = formatted.replaceAll(`%%${item.key.toUpperCase()}%%`, `${item.value}`)) return shellCommand(formatted) } } export abstract class Host implements AwareOfContainerLifecycle { awareOfContainerLifecycle: true = true public abstract getFilesystem(): Awaitable public abstract execute(command: ShellCommand): Awaitable close(): Awaitable {} protected getCommandPalette(): CommandPalette { return new StandardCommandPalette() } onContainerDestroy() { this.close() } onContainerRelease() { this.close() } /** Runs the provided command and throws if it failed. */ public async run(command: ShellCommand): Promise { const result = await this.execute(command) if ( !result.wasSuccessful() ) { throw CommandError.make(command, result) } return result } /** Runs the provided command, throws if it failed, and returns the `infer(...)` result of the first line otherwise. */ public async runLineResult(command: ShellCommand): Promise> { const result = await this.execute(command) if ( !result.wasSuccessful() || result.standardOutput.length < 1 ) { throw CommandError.make(command, result) } return infer(result.standardOutput[0].trim()) } /** Try to ping the host and verify that it is online & accessible. */ public async isAlive(): Promise { const output = uuid4() const cmd = this.getCommandPalette().format('echo', { output }) try { const result = await this.execute(cmd) return result.wasSuccessful() && result.combinedOutput.length > 0 && result.combinedOutput[0] === output } catch (e) { return false } } public async waitForAlive(maxTries: number = 30): Promise { let tries = maxTries while ( tries > 0 ) { if ( await this.isAlive() ) { return } tries -= 1 await new Promise(res => setTimeout(() => res(), 2000)) } throw new ErrorWithContext('Host did not come online before maxTries exceeded', { host: this, maxTries, }) } /** Get the path to a temporary directory on the host. */ public async getTempDirectory(): Promise { const cmd = this.getCommandPalette().format('tempPath', {}) const result = await this.runLineResult(cmd) return new UniversalPath(`${result}`, await this.getFilesystem()) } /** Get the path to a temporary file on the host. */ public async getTempFile(): Promise { const cmd = this.getCommandPalette().format('tempFile', {}) const result = await this.runLineResult(cmd) return new UniversalPath(`${result}`, await this.getFilesystem()) } /** Get a list of filesystem mount points on the host. */ public async getMountPoints(): Promise { const result = await this.run(this.getCommandPalette().format('listMountPoints', {})) return result.standardOutput } /** Load standard metrics about the host. */ public async metrics(): Promise { const metric = new SystemMetrics() const palette = this.getCommandPalette() const cpuCmd = palette.format('cpuPercentage', {}) const cpuResult = await this.runLineResult(cpuCmd) if ( typeof cpuResult === 'number' ) { metric.cpuPercent = cpuResult } const ramCmd = palette.format('ramPercentage', {}) const ramResult = await this.runLineResult(ramCmd) if ( typeof ramResult === 'number' ) { metric.ramPercent = ramResult } const mountPoints = await this.getMountPoints() await Promise.all(mountPoints.map(async mountPoint => { const cmd = palette.format('mountPointPercentage', { mountPoint }) const result = await this.run(cmd) const utilization = Number(result.standardOutput[0].replace('%', '')) / 100 metric.setMount(mountPoint, utilization) })) return metric } /** Get the user/group ownership of the given resource. */ public async getOwnership(path: UniversalPath): Promise { const cmd = this.getCommandPalette().format('fileDirectoryOwnershipFetch', { path: path.toLocal }) const result = `${await this.runLineResult(cmd)}` const parts = result.split(':') return {user: parts[0], group: parts[1]} } /** Set the user/group ownership of the given resource. */ public setOwnership(path: UniversalPath, user: string, group: string, recursive: boolean = false): Awaitable { const cmd = this.getCommandPalette().format( recursive ? 'fileDirectoryOwnershipSetRecursive' : 'fileDirectoryOwnershipSetFlat', { path: path.toLocal, owners: `${user}:${group}`, } ) return this.run(cmd) } /** Retrieve the filesystem permissions for the given resource. */ public async getPermissions(path: UniversalPath): Promise { const cmd = this.getCommandPalette().format('fileDirectoryPermissionFetch', { path: path.toLocal }) return `${await this.runLineResult(cmd)}` } /** Set the filesystem permissions for the given resource. */ public setPermissions(path: UniversalPath, level: string, recursive: boolean = false): Awaitable { const cmd = this.getCommandPalette().format( recursive ? 'fileDirectoryPermissionSetRecursive' : 'fileDirectoryPermissionSetFlat', { path: path.toLocal, level, } ) return this.run(cmd) } /** Reboot the host. */ public reboot(): Awaitable { return this.run(this.getCommandPalette().format('reboot', {})) } }