const { Injectable } = require('flitter-di') const ImplementationError = require('libflitter/errors/ImplementationError') const PermissionDeniedError = require('../logical/error/PermissionDeniedError') const CommandNotFoundError = require('../logical/error/CommandNotFoundError') const FilesystemResourceNotFoundError = require('../logical/error/FilesystemResourceNotFoundError') const uuid = require('uuid').v4 const UniversalPath = require('../logical/UniversalPath') const SystemMetrics = require('../logical/SystemMetrics') const DNFManager = require('../logical/packages/DNFManager') const APTManager = require('../logical/packages/APTManager') const SystemDManager = require('../logical/services/SystemDManager') class Host extends Injectable { static get services() { return [...super.services, 'utility', 'app'] } _temp_path_command = 'mktemp -d' _temp_file_command = 'mktemp' _cpu_percentage_command = `grep 'cpu ' /proc/stat | awk '{usage=($2+$4)*100/($2+$4+$5)} END {print usage}'` _ram_percentage_command = `free | grep Mem | awk '{print $3/$2 * 100.0}'` _mount_point_percentage_command = `df -hl | grep -w '%%MOUNTPOINT%%$' | awk '{print $5}'` _list_mount_points_command = `df -hl | grep '/' | awk '{print $6}'` _file_directory_delete_command = `rm -rf "%%RESOURCE%%"` _resolve_path_command = `readlink -f "%%PATH%%"` _reboot_command = `reboot` _change_directory_command = `cd "%%PATH%%"` _file_directory_permission_fetch_command = `stat -c %a "%%PATH%%"` _file_directory_permission_set_command_flat = `chmod %%LEVEL%% "%%PATH%%"` _file_directory_permission_set_command_recursive = `chmod -R %%LEVEL%% "%%PATH%%"` _file_directory_ownership_fetch_command = `stat -c "%U:%G" "%%PATH%%"` _file_directory_ownership_set_command_flat = `chown %%OWNERS%% "%%PATH%%"` _file_directory_ownership_set_command_recursive = `chown -R %%OWNERS%% "%%PATH%%"` constructor(config) { super() this.config = config this.name = config.name const injector = this.app.di() if ( config.packages && config.packages.type ) { if ( config.packages.type === 'dnf' ) { this.packages = injector.make(DNFManager, this) } else if ( config.packages.type === 'apt' ) { this.packages = injector.make(APTManager, this) } else { throw new Error(`Invalid or unknown package manager type: ${config.packages.type}`) } } if ( config.services && config.services.type ) { if ( config.services.type === 'systemd' ) { this.services = injector.make(SystemDManager, this) } else { throw new Error(`Invalid or unknown service manager type: ${config.services.type}`) } } this.injector = injector } async execute(command) { throw new ImplementationError() } async open_file_read_stream(file_path) { throw new ImplementationError() } async open_file_write_stream(file_path) { throw new ImplementationError() } async is_alive() { try { const unique_id = uuid() const result = await this.execute(`echo "${unique_id}"`) return (result.exit_code === 0 && (result.clean_out.length > 0 && result.clean_out[0] === unique_id)) } catch (e) { return false } } async get_temp_path() { const path_string = await this.run_line_result(this._temp_path_command) return this.injector.make(UniversalPath, this, path_string, UniversalPath.PATH_TYPE_DIRECTORY) // return new UniversalPath(this, path_string, UniversalPath.PATH_TYPE_DIRECTORY) } async get_temp_file() { const file_string = await this.run_line_result(this._temp_file_command) return this.injector.make(UniversalPath, this, file_string, UniversalPath.PATH_TYPE_FILE) // return new UniversalPath(this, file_string, UniversalPath.PATH_TYPE_FILE) } async get_path(local_path) { const host_path = this.injector.make(UniversalPath, this, local_path) // const host_path = new UniversalPath(this, local_path) await host_path.classify() return host_path } async metrics() { const metric = new SystemMetrics() const cpu_percent = Number(await this.run_line_result(this._cpu_percentage_command)) const ram_percent = Number(await this.run_line_result(this._ram_percentage_command)) metric.cpu(cpu_percent) metric.ram(ram_percent) const mount_points = await this.get_mount_points() for ( const point of mount_points ) { metric.mount(point, await this.get_mountpoint_utilization(point)) } return metric } async delete_path(resource_path) { resource_path = typeof resource_path === 'string' ? resource_path : resource_path.path await this.execute(this._file_directory_delete_command.replace('%%RESOURCE%%', resource_path)) } get_directory_change_command(path) { if ( typeof path !== 'string' ) path = path.path return this._change_directory_command.replace('%%PATH%%', path) } async resolve_path(resource_path) { resource_path = typeof resource_path === 'string' ? resource_path : resource_path.path return this.run_line_result(this._resolve_path_command.replace('%%PATH%%', resource_path)) } async get_permissions_for_path(resource_path) { resource_path = typeof resource_path === 'string' ? resource_path : resource_path.path return this.run_line_result(this._file_directory_permission_fetch_command.replace('%%PATH%%', resource_path)) } async set_permissions_for_path(resource_path, level, recursive = false) { resource_path = typeof resource_path === 'string' ? resource_path : resource_path.path let cmd if ( recursive ) cmd = this._file_directory_permission_set_command_recursive else cmd = this._file_directory_permission_set_command_flat cmd = cmd.replace('%%PATH%%', resource_path) cmd = cmd.replace('%%LEVEL%%', level) await this.run(cmd) } async get_ownership_for_path(resource_path) { resource_path = typeof resource_path === 'string' ? resource_path : resource_path.path const cmd = this._file_directory_ownership_fetch_command.replace('%%PATH%%', resource_path) const result = await this.run_line_result(cmd) const parts = result.split(':') return {user: parts[0], group: parts[1]} } async set_ownership_for_path(resource_path, { user, group }, recursive = false) { resource_path = typeof resource_path === 'string' ? resource_path : resource_path.path let cmd if ( recursive ) cmd = this._file_directory_ownership_set_command_recursive else cmd = this._file_directory_ownership_set_command_flat cmd = cmd.replace('%%PATH%%', resource_path) cmd = cmd.replace('%%OWNERS%%', `${user}:${group}`) await this.run(cmd) } async get_mount_points() { const result = await this.execute(this._list_mount_points_command) if ( result.exit_code !== 0 ) { throw new Error('Unable to determine mount points. Command execution error.') } return result.clean_out } async get_mountpoint_utilization(mountpoint) { const cmd = this._mount_point_percentage_command.replace('%%MOUNTPOINT%%', mountpoint) const result = await this.execute(cmd) if ( result.exit_code !== 0 ) { throw new Error('Unable to determine mount utilization. Command execution error.') } return Number(result.clean_out[0].replace('%', ''))/100 } async run_line_result(command) { const result = await this.execute(command) if ( result.exit_code !== 0 || result.clean_out.length < 1 ) { const E = this._get_result_error_class(result) throw new E('Unable to get line output from command: '+command+'\n'+result.clean_err) } return this.utility.infer(result.clean_out[0].trim()) } async run(command) { const result = await this.execute(command) if ( result.exit_code !== 0 ) { const E = this._get_result_error_class(result) throw new E('Unable to run command: '+command+'\n'+result.clean_err) } return result } async list_files_in_directory(local_path) { throw new ImplementationError() } async _cleanup() {} async reboot() { await this.run_line_result(this._reboot_command) } _get_result_error_class(result) { const both = `${result.clean_out}\n${result.clean_err}`.toLowerCase() const access_denied_phrases = [ 'not permitted', 'access denied', 'insufficient permission', 'permission denied' ] const not_found_phrases = [ 'command not found', ] const resource_not_found = [ 'no such file', 'no such directory', 'no such file or directory', 'no such directory or file', ] for ( const phrase of access_denied_phrases ) { if ( both.includes(phrase) ) return PermissionDeniedError } for ( const phrase of not_found_phrases ) { if ( both.includes(phrase) ) return CommandNotFoundError } for ( const phrase of resource_not_found ) { if ( both.includes(phrase) ) return FilesystemResourceNotFoundError } return Error } } module.exports = exports = Host