Initial Commit

master
garrettmills 3 years ago
commit e2016069f2
  1. 90
      .gitignore
  2. 7
      LICENSE
  3. 60
      README.md
  4. 27
      Units.flitter.js
  5. 29
      app/classes/logical/ExecutionResult.js
  6. 24
      app/classes/logical/SystemMetrics.js
  7. 167
      app/classes/logical/UniversalPath.js
  8. 177
      app/classes/logical/packages/APTManager.js
  9. 128
      app/classes/logical/packages/DNFManager.js
  10. 104
      app/classes/logical/packages/PackageManager.js
  11. 111
      app/classes/logical/services/ServiceConfig.js
  12. 79
      app/classes/logical/services/ServiceManager.js
  13. 55
      app/classes/logical/services/SystemDManager.js
  14. 160
      app/classes/metal/Host.js
  15. 30
      app/classes/metal/LocalHost.js
  16. 77
      app/classes/metal/RemoteHost.js
  17. 24
      app/classes/state/State.js
  18. 29
      app/classes/state/fs/DirectoryState.js
  19. 64
      app/classes/state/fs/FileContentState.js
  20. 29
      app/classes/state/fs/FileState.js
  21. 41
      app/services/hosts.service.js
  22. 11
      config/app.config.js
  23. 32
      config/hosts.config.js
  24. 22
      config/server.config.js
  25. 21
      docker-compose.yml
  26. 17
      docker.env
  27. 17
      example.env
  28. 37
      flaps.json
  29. 1
      flaps/auth.json
  30. 12
      flaps/config.json
  31. 12
      flaps/database.json
  32. 7
      flaps/express.json
  33. 7
      flaps/flap.json
  34. 7
      flaps/middleware.json
  35. 7
      flaps/utility.json
  36. 7
      flaps/views.json
  37. 24
      flitter
  38. 29
      index.js
  39. 35
      package.json
  40. 3085
      yarn.lock

90
.gitignore vendored

@ -0,0 +1,90 @@
# Created by https://www.gitignore.io/api/node
# Edit at https://www.gitignore.io/?templates=node
.idea
.idea/*
### Node ###
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
# next.js build output
.next
# nuxt.js build output
.nuxt
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# End of https://www.gitignore.io/api/node

@ -0,0 +1,7 @@
Copyright 2019 Garrett L. Mills.
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

@ -0,0 +1,60 @@
<p align="center"><img height="200" src="https://static.glmdev.tech/flitter/flitter-big.png"></p>
# Flitter
Flitter is a quick & ligthweight web app framework based on Express.
### What?
Flitter is an MVC style framework that aims to get you up and running faster by providing a structure and a wrapper for Express.js. Files in predictable directories are parsed into routes, middleware, controllers, models, and views.
Flitter provides access to the Express app, while making it possible to create an app without needing to piece together the Express framework.
### Flitter Provides:
- Express for routing
- Mongoose for ODM
- Busboy for request parsing
- Favicon support
- `./flitter` - CLI tools for Flitter (including an interactive shell)
- User auth & sessions (see below)
### How?
Getting started with Flitter is easy. To create a new project, simply run the following commands:
```
# Download Flitter:
git clone https://git.glmdev.tech/flitter/flitter {project_name}
cd {project_name}
# Install dependencies:
yarn install
# Create default config:
cp example.env .env
# Launch Flitter!
node index.js
# Or use the interactive shell
./flitter shell
```
And voilà! You should have a Flitter app up and running on port `8000` by default.
### Why?
Flitter's creator is a former Laravel junkie, but loves Node and Express. He got tired of having to hammer out the same 500 lines of code to start every project, but didn't want the bulk and obfuscation of larger frameworks like AdonisJS.
Flitter is designed to be compartmentalized and easy to understand. Every piece of its core functionality is broken into "units." Each of these units does some task like loading config, parsing middleware, connecting to the database, etc. You can see exactly what units your application is loading by viewing the Units file in `config/Units.flitter.js`. Each of Flitters core units are open to view in the [libflitter](https://www.npmjs.com/package/libflitter) package.
Of course, this also means that Flitter is extremely easy to extend. If you want to add a custom package, simply require it and add its unit to the Units file!
### Who?
Flitter was created by [Garrett Mills](https://glmdev.tech/), and its use is governed by the terms of the MIT License as specified in the LICENSE file.
Of course, that does mean that Flitter is © 2019 Garrett Mills. ;)
This command will copy the necessary files to your Flitter install. The files are directly accessible and, therefore, completely customizable.

@ -0,0 +1,27 @@
/*
* The Flitter Units File
* -------------------------------------------------------------
* Flitter uses a unit-chain style initialization system. This means that
* individual components of Flitter and its add-ons are specified in order
* here. Then, when the app is created, Flitter creates a single functional
* chain by passing the next unit to the current unit's loading script. This
* launches Flitter with a single function call (FlitterApp.up()) and enables
* developers to contextualize Flitter within async or callback functions.
*/
const FlitterUnits = {
/*
* The Core Flitter Units
* -------------------------------------------------------------
* These units comprise the core functionality of Flitter. Unless you
* really know what you are doing, you should NEVER change them.
*/
'Canon' : require('libflitter/canon/CanonicalAccessUnit'),
'Config' : require('libflitter/config/ConfigUnit'),
'Services' : require('libflitter/services/ServicesUnit'),
'Utility' : require('libflitter/utility/UtilityUnit'),
'Cli' : require('flitter-cli/CliUnit'),
'App' : require('flitter-cli/CliAppUnit'),
}
module.exports = exports = FlitterUnits

@ -0,0 +1,29 @@
const { Injectable } = require('flitter-di')
class ExecutionResult extends Injectable {
stdout = []
stderr = []
exit_code = 0
out(data) {
this.stdout = this.stdout.concat(String(data).split('\n'))
}
error(data) {
this.stderr = this.stderr.concat(String(data).split('\n'))
}
exit(code) {
this.exit_code = Number(code)
}
get clean_out() {
return this.stdout.filter(Boolean)
}
get clean_err() {
return this.stdout.filter(Boolean)
}
}
module.exports = exports = ExecutionResult

@ -0,0 +1,24 @@
const { Injectable } = require('flitter-di')
class SystemMetrics extends Injectable {
_cpu = 0
_ram = 0
_mounts = {}
cpu(set = false) {
if ( set !== false ) this._cpu = set
else return this._cpu
}
ram(set = false) {
if ( set !== false ) this._ram = set
else return this._ram
}
mount(point, set = false) {
if ( set !== false ) this._mounts[point] = set
else return this._mounts[point]
}
}
module.exports = exports = SystemMetrics

@ -0,0 +1,167 @@
const { Injectable } = require('flitter-di')
const moment = require('moment')
class UniversalPath extends Injectable {
static get services() {
return [...super.services, 'hosts']
}
static PATH_TYPE_FILE = Symbol('file')
static PATH_TYPE_DIRECTORY = Symbol('directory')
static PATH_TYPE_UNKNOWN = Symbol('unknown')
_directory_classify_command = 'test -d'
_file_classify_command = 'test -f'
_datetime_format = 'Y-MM-DD_HH-mm-ss-SSSS'
static fromString(path_string) {
const parts = path_string.split('::')
const hostname = parts.length > 1 ? parts[0] : 'localhost'
const path = parts.length > 1 ? parts[1] : parts[0]
const host = this.prototype.hosts.get(hostname)
return new this(host, path)
}
constructor(host, host_path, type) {
super()
this._host = host
this._path = host_path
this._type = type ? type : this.constructor.PATH_TYPE_UNKNOWN
}
get host() {
return this._host
}
get path() {
return this._path
}
get type() {
return this._type
}
async resolve() {
return this._host.resolve_path(this._path)
}
is_directory() {
return this.type === this.constructor.PATH_TYPE_DIRECTORY
}
is_file() {
return this.type === this.constructor.PATH_TYPE_FILE
}
is_valid() {
return this.is_file() || this.is_directory()
}
async classify() {
const dir_result = await this._host.execute(`${this._directory_classify_command} ${this._path}`)
if ( dir_result.exit_code === 0 ) {
this._type = this.constructor.PATH_TYPE_DIRECTORY
return
}
const file_result = await this._host.execute(`${this._file_classify_command} ${this._path}`)
if ( file_result.exit_code === 0 ) {
this._type = this.constructor.PATH_TYPE_FILE
return
}
this._type = this.constructor.PATH_TYPE_UNKNOWN
}
async open_read_stream() {
await this.classify()
if ( this._type === this.constructor.PATH_TYPE_FILE ) {
return this._host.open_file_read_stream(this._path)
} else {
throw new Error('Maestro does not support generation of read streams for this type of path: '+this._type.description)
}
}
async open_write_stream() {
await this.classify()
if (
[this.constructor.PATH_TYPE_FILE, this.constructor.PATH_TYPE_UNKNOWN].includes(this._type)
) {
return this._host.open_file_write_stream(this._path)
} else {
throw new Error('Maestro does not support generation of write streams for this type of path: '+this._type.description)
}
}
async copy_from(other_path) {
const read = await other_path.open_read_stream()
const write = await this.open_write_stream()
await read.pipe(write)
}
async copy_to(other_path) {
const read = await this.open_read_stream()
const write = await other_path.open_write_stream()
await read.pipe(write)
}
async unlink() {
await this.host.delete_path(this._path)
}
async move_from(other_path) {
await this.copy_from(other_path)
await other_path.unlink()
}
async move_to(other_path) {
await this.copy_to(other_path)
await this.unlink()
}
async touch(directory = false) {
await this.classify()
if ( !directory ) {
const base_dir = this._path.split('/').slice(0, -1).join('/')
await this._host.run(`mkdir -p ${base_dir}`)
await this._host.run(`touch ${this._path}`)
} else {
await this._host.run(`mkdir -p ${this._path}`)
}
}
async echo(text) {
await this.classify()
if ( this.is_directory() ) {
throw new Error('Cannot echo to a directory.')
}
if ( !this.is_valid() ) {
await this.touch()
}
const ws = await this.open_write_stream()
ws.write(text)
return new Promise(resolve => {
ws.on('finish', () => resolve())
ws.end()
})
}
async hash() {
const line = await this._host.run_line_result(`sha256sum ${this._path}`)
return line.split(' ')[0]
}
async backup() {
const now = moment().format(this._datetime_format)
const backup_path = `${this}.maestro-backup-${now}`
const backup_target = this.constructor.fromString(backup_path)
await this.copy_to(backup_target)
}
toString() {
return `${this._host.name}::${this._path}`
}
}
module.exports = exports = UniversalPath

@ -0,0 +1,177 @@
const PackageManager = require('./PackageManager')
class APTManager extends PackageManager {
_command_install_package = 'apt-get install -y %%PACKAGE%%'
_command_uninstall_package = 'apt-get purge -y %%PACKAGE%%'
_command_update_package = 'apt-get upgrade -y %%PACKAGE%%'
_command_reinstall_package = 'apt-get install -y --reinstall %%PACKAGE%%'
_command_clear_cache = 'apt-get clean'
_command_count_installed = 'dpkg --list | wc -l'
_command_count_available = 'apt-cache pkgnames | wc -l'
_command_add_repo = `add-apt-repository '%%URI%%`
_command_preflight = 'apt-get update'
_status_keymap = {
package: 'name',
filename: 'source',
section: 'repository',
'description-en': 'summary',
}
async status(pkg) {
await this.preflight()
const result = await this._host.execute(`apt-cache show ${pkg}`)
if ( ![0, 100].includes(result.exit_code) ) {
throw new Error('Unable to determine package information: '+result.stderr.join('\n'))
}
if ( result.exit_code === 100 ) {
return {
name: pkg,
state: this.constructor.PACKAGE_STATE_UNKNOWN,
}
}
const data = {}
let last_data_key = false
for ( const line of result.clean_out ) {
if ( last_data_key && line.startsWith(' ') ) {
data[last_data_key] += line
} else {
const parts = line.trim().split(':')
const key = parts[0].toLowerCase()
const value = parts.slice(1).join(':').trim()
data[key] = value
last_data_key = key
}
}
for ( const key in this._status_keymap ) {
if ( !this._status_keymap.hasOwnProperty(key) ) continue
if ( data[key] ) {
data[this._status_keymap[key]] = data[key]
delete data[key]
}
}
// check installation state
const install_result = await this._host.execute(`dpkg -s ${pkg}`)
data.state = install_result.exit_code === 0 ? this.constructor.PACKAGE_STATE_INSTALLED : this.constructor.PACKAGE_STATE_AVAILABLE
return data
}
async list_updates() {
await this.preflight()
const result = await this._host.execute(`apt-get --just-print upgrade`)
if ( result.exit_code !== 0 ) {
throw new Error('Unable to determine packages for update: '+result.stderr.join('\n'))
}
let start_index = -1
let end_index = -1
for ( let i in result.clean_out ) {
const line = result.clean_out[i]
if ( line.toLowerCase().indexOf('packages will be upgraded') > -1 ) {
start_index = Number(i)+1
} else if ( start_index > -1 && i >= start_index ) {
if ( line.indexOf('newly installed,') > -1 ) break
else {
end_index = Number(i)
}
}
}
const package_strings = result.clean_out.slice(start_index, end_index+1).join(' ').split(' ').filter(Boolean).map(x => x.trim())
const updates = result.clean_out.filter(x => x.startsWith('Inst ')).map(x => {
x = x.substring(5)
x = x.split(' ')
const data = {
name: x[0],
version: x[1].slice(1, -1),
repository: x[3].replace(/,/g, ''),
architecture: x.reverse()[0].replace(/[\[\])(]/g, '')
}
return data
}).filter(x => package_strings.includes(x.name))
return updates
}
async list_repos() {
await this.preflight()
const result = await this._host.execute(`apt-cache policy`)
if ( result.exit_code !== 0 ) {
throw new Error('Unable to read repository list: '+result.stderr.join('\n'))
}
const lines = []
for ( const line of result.clean_out ) {
if ( line.trim().startsWith('500') ) {
lines.push(line)
} else if ( line.toLowerCase().startsWith('pinned packages') ) {
break
}
}
return lines.map(x => x.trim()).filter(x => x.startsWith('500')).map(x => x.split(' ')[2])
}
async list_installed() {
const result = await this._host.execute(`dpkg --list`)
if ( result.exit_code !== 0 ) {
throw new Error('Unable to determine installed packages: '+result.stderr.join('\n'))
}
let header_line = result.clean_out.filter(x => x.trim().startsWith('+'))[0]
let drop_count = 0
while ( header_line.startsWith('+') ) {
drop_count += 1
header_line = header_line.substring(1)
}
let start_at = 0
for ( let i in result.clean_out ) {
if ( result.clean_out[i].startsWith('+') ) {
start_at = Number(i)+1
break
}
}
return result.clean_out.slice(start_at).map(x => {
x = x.substring(drop_count).trim().split(' ').filter(Boolean)
return {
name: x[0],
version: x[1],
architecture: x[2],
summary: x.slice(3).join(' ')
}
})
}
async search(pkg) {
await this.preflight()
const result = await this._host.execute(`apt-cache search ${pkg}`)
if ( result.exit_code !== 0 ) {
throw new Error('Unable to search the package cache: '+result.stderr.join('\n'))
}
const results = []
for ( const line of result.clean_out ) {
const parts = line.trim().split(' - ').map(x => x.trim())
results.push({
name: parts[0],
summary: parts[1]
})
}
return results
}
async preflight() {
const result = await this._host.execute(this._command_preflight)
if ( result.exit_code !== 0 ) {
throw new Error('Error encountered during preflight for APTManager: '+result.stderr.join('\n'))
}
}
}
module.exports = exports = APTManager

@ -0,0 +1,128 @@
const PackageManager = require('./PackageManager')
class DNFManager extends PackageManager {
_command_install_package = 'dnf install -y %%PACKAGE%%'
_command_uninstall_package = 'dnf remove -y %%PACKAGE%%'
_command_update_package = 'dnf update -y %%PACKAGE%%'
_command_reinstall_package = 'dnf reinstall -y %%PACKAGE%%'
_command_add_repo = 'dnf config-manager -y --add-repo="%%URI%%"'
_command_clear_cache = 'dnf clean all'
_command_count_installed = 'dnf list installed -q | wc -l'
_command_count_available = 'dnf list available -q | wc -l'
async status(pkg) {
const result = await this._host.execute(`dnf info -q --installed ${pkg}`)
if ( result.exit_code === 0 ) {
const data = this._data_from_result(result.clean_out)
data.state = this.constructor.PACKAGE_STATE_INSTALLED
return data
} else {
const available_result = await this._host.execute(`dnf info -q --available ${pkg}`)
if ( available_result.exit_code === 0 ) {
const data = this._data_from_result(available_result.clean_out)
data.state = this.constructor.PACKAGE_STATE_AVAILABLE
return data
} else {
return {
name: pkg,
state: this.constructor.PACKAGE_STATE_UNKNOWN,
}
}
}
}
async list_updates() {
const result = await this._host.execute(`dnf check-update -q`)
// 100 is the known exit code for successful result, but updates pending
if ( ![0, 100].includes(result.exit_code) ) {
throw new Error('Unable to check for updates: '+result.stderr.join('\n'))
}
const updates = []
for ( const line of result.clean_out ) {
const parts = line.trim().split(' ').filter(Boolean)
const data = this._package_info_from_line_parts(parts)
updates.push(data)
}
return updates
}
async list_repos() {
const result = await this._host.execute(`dnf repolist all -q`)
if ( result.exit_code !== 0 ) {
throw new Error('Unable to determine repositories: '+result.stderr.join('\n'))
}
const offset_length = result.clean_out[0].toLowerCase().indexOf('repo name')
return result.clean_out.slice(1).map(line => line.substr(0, offset_length).trim())
}
async list_installed() {
const result = await this._host.execute(`dnf list installed -q`)
if ( result.exit_code !== 0 ) {
throw new Error('Unable to determine installed packages: '+result.stderr.join('\n'))
}
const results = []
for ( const line of result.clean_out.slice(1) ) {
const parts = line.trim().split(' ').filter(Boolean)
const data = this._package_info_from_line_parts(parts)
results.push(data)
}
return results
}
async search(term) {
const result = await this._host.execute(`dnf search ${term} -q`)
if ( result.exit_code !== 0 ) {
throw new Error('Unable to complete search: '+result.stderr.join('\n'))
}
const results = []
for ( const line of result.clean_out ) {
if ( !line.trim().startsWith('=') ) {
const parts = line.split(':').map(x => x.trim())
const data = this._package_info_from_line_parts(parts)
if ( data.version ) {
data.summary = data.version
delete data.version
}
results.push(data)
}
}
return results
}
_data_from_result(stdout) {
const data = {}
for ( const line of stdout ) {
const parts = line.split(':').map(x => String(x).trim())
if ( parts.length > 1 && parts[0] ) {
let key = parts[0].toLowerCase()
if ( key === 'from repo' ) key = 'repo'
data[key] = parts.slice(1).join(':')
}
}
return data
}
_package_info_from_line_parts(parts) {
const data = {}
if ( parts.length > 0 ) {
data.name = parts[0]
if ( data.name.indexOf('.') >= 0 ) {
const arch_parts = data.name.split('.').reverse()
data.architecture = arch_parts[0]
data.name = arch_parts.slice(1).reverse().join('.')
}
}
if ( parts.length > 1 ) data.version = parts[1]
if ( parts.length > 2 ) data.repository = parts[2]
return data
}
}
module.exports = exports = DNFManager

@ -0,0 +1,104 @@
const { Injectable } = require('flitter-di')
const ImplementationError = require('libflitter/errors/ImplementationError')
class PackageManager extends Injectable {
static get services() {
return [...super.services, 'hosts']
}
static PACKAGE_STATE_INSTALLED = 'installed'
static PACKAGE_STATE_AVAILABLE = 'available'
static PACKAGE_STATE_UNKNOWN = 'unknown'
_command_install_package = '%%PACKAGE%%'
_command_uninstall_package = '%%PACKAGE%%'
_command_update_package = '%%PACKAGE%%'
_command_reinstall_package = '%%PACKAGE%%'
_command_add_repo = '%%URI%%'
_command_clear_cache = ''
_command_count_installed = ''
_command_count_available = ''
_group_packages_by = ' '
constructor(host_or_hostname) {
super()
if ( typeof host_or_hostname === 'string' ) {
host_or_hostname = this.hosts.get(host_or_hostname)
}
this._host = host_or_hostname
}
async list_updates() {
throw new ImplementationError()
}
async list_repos() {
throw new ImplementationError()
}
async status(pkg) {
throw new ImplementationError()
}
async search(term) {
throw new ImplementationError()
}
async list_installed() {
throw new ImplementationError()
}
async is_installed(pkg) {
const result = await this.status(pkg)
return result.state === this.constructor.PACKAGE_STATE_INSTALLED
}
async clear_cache() {
await this._host.run_line_result(this._command_clear_cache)
}
async count_installed() {
return this._host.run_line_result(this._command_count_installed)
}
async count_available() {
return this._host.run_line_result(this._command_count_available)
}
async install(...packages) {
await this._standard_format_execution(packages, this._command_install_package)
}
async uninstall(...packages) {
await this._standard_format_execution(packages, this._command_uninstall_package)
}
async update(...packages) {
await this._standard_format_execution(packages, this._command_update_package)
}
async reinstall(...packages) {
await this._standard_format_execution(packages, this._command_reinstall_package)
}
async add_repo(uri) {
await this._standard_format_execution([uri], this._command_add_repo, '%%URI%%')
}
async _standard_format_execution(packages, command, placeholder = '%%PACKAGE%%') {
if ( this._group_packages_by ) {
packages = [packages.join(this._group_packages_by)]
}
for ( const package_name of packages ) {
const result = await this._host.execute(command.replace(placeholder, package_name))
if ( result.exit_code !== 0 ) {
throw new Error(`Error encountered while executing command (${command.replace(placeholder, package_name)}): ${result.stderr.join('\n')}`)
}
}
}
}
module.exports = exports = PackageManager

@ -0,0 +1,111 @@
const { Injectable } = require('flitter-di')
class ServiceConfig extends Injectable {
static get services() {
return [...super.services, 'utility']
}
_data = {
after: ['syslog.target', 'network.target'],
type: 'simple',
restart: 'always',
restart_after: '2s',
install_to: 'multi-user.target',
}
name(set) { return this._get_or_set('name', set) }
description(set) { return this._get_or_set('description', set) }
after(set) { return this._get_or_set('after', set, true) }
restart(set) { return this._get_or_set('restart', set) }
restart_after(set) { return this._get_or_set('restart_after', set) }
type(set) { return this._get_or_set('type', set) }
user(set) { return this._get_or_set('user', set) }
group(set) { return this._get_or_set('group', set) }
directory(set) { return this._get_or_set('directory', set) }
execute(set) { return this._get_or_set('execute', set) }
env(set) { return this._get_or_set('env', set) }
install_to(set) { return this._get_or_set('install_to', set) }
_export() {
return this.utility.deep_copy(this._data)
}
_build() {
this._validate()
const lines = []
lines.push('[Unit]')
if ( this.description() ) lines.push(`Description=${this.description()}`)
if ( this.after() ) {
for ( const item of this.after() ) {
lines.push(`After=${item}`)
}
}
lines.push('\n[Service]')
if ( this.restart() ) {
lines.push(`Restart=${this.restart()}`)
if ( this.restart_after() ) {
lines.push(`RestartSec=${this.restart_after()}`)
}
}
if ( this.user() ) {
lines.push(`User=${this.user()}`)
}
if ( this.group() ) {
lines.push(`Group=${this.group()}`)
}
if ( this.directory() ) {
lines.push(`WorkingDirectory=${this.directory()}`)
}
if ( this.execute() ) {
lines.push(`ExecStart=${this.execute()}`)
}
if ( this.env() ) {
lines.push(`Environment=${this.env()}`)
}
if ( this.install_to() ) {
lines.push('\n[Install]')
lines.push(`WantedBy=${this.install_to()}`)
}
return lines.join('\n')
}
_validate() {
const required_fields = ['name', 'description', 'after', 'type', 'user', 'group', 'directory', 'execute']
for ( const field of required_fields ) {
if ( !this[field]() ) throw new Error(`Missing required service config field: ${field}`)
}
}
_get_or_set(field, value, arr = false) {
if ( value ) {
if ( arr ) {
if ( !this._data[field] ) this._data[field] = []
this._data[field].push(value)
} else this._data[field] = value
return this
}
return this._data[field]
}
}
module.exports = exports = ServiceConfig

@ -0,0 +1,79 @@
const { Injectable } = require('flitter-di')
const ImplementationError = require('libflitter/errors/ImplementationError')
const ServiceConfig = require('./ServiceConfig')
class ServiceManager extends Injectable {
static SERVICE_STATE_STOPPED = 'stopped'
static SERVICE_STATE_ERROR = 'error'
static SERVICE_STATE_RUNNING = 'running'
_command_restart_service = '%%SERVICE%%'
_command_start_service = '%%SERVICE%%'
_command_stop_service = '%%SERVICE%%'
_command_reload_service = '%%SERVICE%%'
_command_enable_service = '%%SERVICE%%'
_command_disable_service = '%%SERVICE%%'
_command_daemon_reload = ''
_group_services_by = ' '
constructor(host) {
super()
this._host = host
}
async status(...services) {
throw new ImplementationError()
}
async install(config) {
throw new ImplementationError()
}
new_config() {
return new ServiceConfig()
}
async start(...services) {
await this._standard_format_execution(services, this._command_start_service)
}
async stop(...services) {
await this._standard_format_execution(services, this._command_stop_service)
}
async restart(...services) {
await this._standard_format_execution(services, this._command_restart_service)
}
async reload(...services) {
await this._standard_format_execution(services, this._command_reload_service)
}
async enable(...services) {
await this._standard_format_execution(services, this._command_enable_service)
}
async disable(...services) {
await this._standard_format_execution(services, this._command_disable_service)
}
async daemon_reload() {
await this._host.run_line_result(this._command_daemon_reload)
}
async _standard_format_execution(items, command, replace = '%%SERVICE%%') {
if ( this._group_services_by ) {
items = [items.join(this._group_services_by)]
}
for ( const item of items ) {
const result = await this._host.execute(command.replace(replace, item))
if ( result.exit_code !== 0 ) {
throw new Error('Error encountered while executing command: '+result.stderr.join('\n'))
}
}
}
}
module.exports = exports = ServiceManager

@ -0,0 +1,55 @@
const ServiceManager = require('./ServiceManager')
class SystemDManager extends ServiceManager {
_command_restart_service = 'systemctl restart %%SERVICE%%'
_command_start_service = 'systemctl start %%SERVICE%%'
_command_stop_service = 'systemctl stop %%SERVICE%%'
_command_reload_service = 'systemctl reload %%SERVICE%%'
_command_enable_service = 'systemctl enable %%SERVICE%%'
_command_disable_service = 'systemctl disable %%SERVICE%%'
_command_daemon_reload = 'systemctl daemon-reload'
async status(...services) {
const statuses = []
for ( const service_name of services ) {
const result = await this._host.execute(`systemctl status -l --value --no-pager ${service_name}`)
if ( result.exit_code !== 0 ) {
throw new Error(`Unable to determine service state: ${result.stderr.join('\n')}`)
}
const status = result.clean_out.map(x => x.trim().toLowerCase())
.filter(x => x.startsWith('active:'))[0]
.split(':')[1]
.trim()
statuses.push({
name: service_name,
status: this._determine_status_from_string(status)
})
}
return statuses.length === 1 ? statuses[0] : statuses
}
async install(config) {
if ( !config._build || !config.name ) throw new Error('Config must be instance of classes/logical/services/ServiceConfig')
const file_contents = config._build()
const name = config.name()
const service_file = await this._host.get_path(`/etc/systemd/system/${name}.service`)
await service_file.echo(file_contents)
}
_determine_status_from_string(str) {
if ( str.startsWith('active') ) {
return this.constructor.SERVICE_STATE_RUNNING
} else if ( str.startsWith('fail') ) {
return this.constructor.SERVICE_STATE_ERROR
} else {
return this.constructor.SERVICE_STATE_STOPPED
}
}
}
module.exports = exports = SystemDManager

@ -0,0 +1,160 @@
const { Injectable } = require('flitter-di')
const ImplementationError = require('libflitter/errors/ImplementationError')
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']
}
_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`
constructor(config) {
super()
this.config = config
this.name = config.name
if ( config.packages && config.packages.type ) {
if ( config.packages.type === 'dnf' ) {
this.packages = new DNFManager(this)
} else if ( config.packages.type === 'apt' ) {
this.packages = new 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 = new SystemDManager(this)
} else {
throw new Error(`Invalid or unknown service manager type: ${config.services.type}`)
}
}
}
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 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 new UniversalPath(this, file_string, UniversalPath.PATH_TYPE_FILE)
}
async get_path(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))
}
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_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 ) {
throw new Error('Unable to get line output from command: '+command)
}
return this.utility.infer(result.clean_out[0].trim())
}
async run(command) {
const result = await this.execute(command)
if ( result.exit_code !== 0 ) {
throw new Error('Unable to run command: '+command)
}
return result
}
async list_files_in_directory(local_path) {
throw new ImplementationError()
}
async _cleanup() {}
async reboot() {
await this.run_line_result(this._reboot_command)
}
}
module.exports = exports = Host

@ -0,0 +1,30 @@
const Host = require('./Host')
const child_process = require('child_process')
const ExecutionResult = require('../logical/ExecutionResult')
const fs = require('fs')
class LocalHost extends Host {
async execute(command) {
const result = new ExecutionResult()
return new Promise((resolve) => {
child_process.exec(command, (error, stdout, stderr) => {
result.exit(error ? error.code : 0)
result.out(stdout)
result.error(stderr)
resolve(result)
})
})
}
async open_file_read_stream(file_path) {
file_path = typeof file_path === 'string' ? file_path : file_path.path
return fs.createReadStream(file_path)
}
async open_file_write_stream(file_path) {
file_path = typeof file_path === 'string' ? file_path : file_path.path
return fs.createWriteStream(file_path)
}
}
module.exports = exports = LocalHost

@ -0,0 +1,77 @@
const Host = require('./Host')
const ssh = require('ssh2')
const ExecutionResult = require('../logical/ExecutionResult')
const fs = require('fs').promises
class RemoteHost extends Host {
_ssh = false
async execute(command) {
const conn = await this._get_ssh_connection()
const result = new ExecutionResult()
await new Promise((resolve, reject) => {
conn.exec(command, (error, stream) => {
if ( error ) reject(error)
else {
stream.on('close', (code, signal) => {
result.exit(code)
resolve()
}).on('data', (data) => {
result.out(data)
}).stderr.on('data', (data) => {
result.error(data)
})
}
})
})
return result
}
async open_file_read_stream(file_path) {
file_path = typeof file_path === 'string' ? file_path : file_path.path
const sftp = await this._get_sftp_connection()
return sftp.createReadStream(file_path)
}
async open_file_write_stream(file_path) {
file_path = typeof file_path === 'string' ? file_path : file_path.path
const sftp = await this._get_sftp_connection()
return sftp.createWriteStream(file_path)
}
async _get_ssh_connection() {
if ( this._ssh ) return this._ssh
if ( this.config.ssh_params && this.config.ssh_params.key_file && !this.config.ssh_params.privateKey ) {
this.config.ssh_params.privateKey = await fs.readFile(this.config.ssh_params.key_file)
}
return new Promise((resolve, reject) => {
const client = new ssh.Client()
client.on('ready', () => {
this._ssh = client
resolve(client)
}).connect(this.config.ssh_params)
client.on('error', reject)
})
}
async _get_sftp_connection() {
const ssh = await this._get_ssh_connection()
return new Promise((resolve, reject) => {
ssh.sftp((err, sftp) => {
if ( err ) reject(err)
else resolve(sftp)
})
})
}
async _cleanup() {
if ( this._ssh ) {
this._ssh.end()
}
}
}
module.exports = exports = RemoteHost

@ -0,0 +1,24 @@
const { Injectable } = require('flitter-di')
const ImplementationError = require('libflitter/errors/ImplementationError')
class State extends Injectable {
constructor(host, config) {
super()
this._host = host
this._config = config
}
async apply() {
throw new ImplementationError()
}
async check() {
throw new ImplementationError()
}
async reverse() {
throw new ImplementationError()
}
}