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
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', {}))
|
|
}
|
|
}
|