You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

204 lines
7.9 KiB

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<string, any>): 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<Filesystem>
public abstract execute(command: ShellCommand): Awaitable<ExecutionResult>
close(): Awaitable<void> {}
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<ExecutionResult> {
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<ReturnType<typeof infer>> {
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<boolean> {
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<void> {
let tries = maxTries
while ( tries > 0 ) {
if ( await this.isAlive() ) {
return
}
tries -= 1
await new Promise<void>(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<UniversalPath> {
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<UniversalPath> {
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<string[]> {
const result = await this.run(this.getCommandPalette().format('listMountPoints', {}))
return result.standardOutput
}
/** Load standard metrics about the host. */
public async metrics(): Promise<SystemMetrics> {
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<OwnershipSpec> {
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<ExecutionResult> {
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<string> {
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<ExecutionResult> {
const cmd = this.getCommandPalette().format(
recursive ? 'fileDirectoryPermissionSetRecursive' : 'fileDirectoryPermissionSetFlat',
{
path: path.toLocal,
level,
}
)
return this.run(cmd)
}
/** Reboot the host. */
public reboot(): Awaitable<ExecutionResult> {
return this.run(this.getCommandPalette().format('reboot', {}))
}
}