Add support for routines; state messages
This commit is contained in:
parent
e401809ad5
commit
8319859828
@ -20,6 +20,7 @@ const FlitterUnits = {
|
||||
'Config' : require('libflitter/config/ConfigUnit'),
|
||||
'Services' : require('libflitter/services/ServicesUnit'),
|
||||
'Utility' : require('libflitter/utility/UtilityUnit'),
|
||||
'Notify' : require('flitter-gotify/src/unit/NotifyUnit'),
|
||||
'Cli' : require('flitter-cli/CliUnit'),
|
||||
'App' : require('flitter-cli/CliAppUnit'),
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ class APTManager extends PackageManager {
|
||||
_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_add_repo = `add-apt-repository '%%URI%%'`
|
||||
_command_preflight = 'apt-get update'
|
||||
|
||||
_status_keymap = {
|
||||
|
62
app/classes/routine/Routine.js
Normal file
62
app/classes/routine/Routine.js
Normal file
@ -0,0 +1,62 @@
|
||||
const { Injectable } = require('flitter-di')
|
||||
const RoutineExecutionResult = require('./RoutineExecutionResult')
|
||||
const StepResult = require('./StepResult')
|
||||
const InvalidRoutineTypeError = require('./error/InvalidRoutineTypeError')
|
||||
|
||||
class Routine extends Injectable {
|
||||
static get services() {
|
||||
return [...super.services, 'states', 'app']
|
||||
}
|
||||
|
||||
_config
|
||||
_hosts
|
||||
_type
|
||||
|
||||
constructor(hosts, config, type = 'checks') {
|
||||
super()
|
||||
this.app.make(StepResult)
|
||||
this.app.make(RoutineExecutionResult)
|
||||
|
||||
this._config = config
|
||||
this._hosts = hosts
|
||||
this._type = type
|
||||
}
|
||||
|
||||
async execute() {
|
||||
const result = await this._build_result()
|
||||
|
||||
for ( const step of result.steps ) {
|
||||
if ( this._type === 'checks' ) {
|
||||
step.status = (await step.step.check()) ? 'success' : 'fail'
|
||||
step.message = step.status === 'success' ? 'Check passed.' : step.step.check_message()
|
||||
} else if ( this._type === 'apply' ) {
|
||||
if ( !(await step.step.check()) ) {
|
||||
await step.step.apply()
|
||||
step.status = (await step.step.check()) ? 'success' : 'fail'
|
||||
step.message = step.status === 'success' ? 'State applied successfully.' : step.step.failure_message()
|
||||
} else {
|
||||
step.status = 'success'
|
||||
step.message = 'Check passed.'
|
||||
}
|
||||
} else {
|
||||
throw new InvalidRoutineTypeError(this._type)
|
||||
}
|
||||
}
|
||||
|
||||
result.overall_state = result.steps.every(x => x.status === 'success') ? 'success' : 'fail'
|
||||
return result
|
||||
}
|
||||
|
||||
async _build_result() {
|
||||
const steps = []
|
||||
for ( const step_config of this._config.steps ) {
|
||||
const step = this.states.from_config(this._hosts[step_config.host], step_config)
|
||||
const result = new StepResult(this, step)
|
||||
steps.push(result)
|
||||
}
|
||||
|
||||
return new RoutineExecutionResult(steps)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = exports = Routine
|
23
app/classes/routine/RoutineExecutionResult.js
Normal file
23
app/classes/routine/RoutineExecutionResult.js
Normal file
@ -0,0 +1,23 @@
|
||||
const { Injectable } = require('flitter-di')
|
||||
|
||||
class RoutineExecutionResult extends Injectable {
|
||||
steps = []
|
||||
overall_state = 'pending' // pending | success | fail
|
||||
|
||||
constructor(steps = []) {
|
||||
super()
|
||||
this.steps = steps
|
||||
}
|
||||
|
||||
get status() {
|
||||
if ( this.steps.some(x => x.status === 'pending') ) return 'pending'
|
||||
else if ( this.steps.some(x => x.status === 'fail') ) return 'fail'
|
||||
else return 'success'
|
||||
}
|
||||
|
||||
failures() {
|
||||
return this.steps.filter(x => x.status === 'fail')
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = exports = RoutineExecutionResult
|
16
app/classes/routine/StepResult.js
Normal file
16
app/classes/routine/StepResult.js
Normal file
@ -0,0 +1,16 @@
|
||||
const { Injectable } = require('flitter-di')
|
||||
|
||||
class StepResult extends Injectable {
|
||||
step
|
||||
routine
|
||||
status = 'pending' // pending | success | fail
|
||||
message = ''
|
||||
|
||||
constructor(routine, step) {
|
||||
super()
|
||||
this.routine = routine
|
||||
this.step = step
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = exports = StepResult
|
7
app/classes/routine/error/InvalidRoutineTypeError.js
Normal file
7
app/classes/routine/error/InvalidRoutineTypeError.js
Normal file
@ -0,0 +1,7 @@
|
||||
class InvalidRoutineTypeError extends Error {
|
||||
constructor(routine_type) {
|
||||
super(`Invalid routine type: ${routine_type}`)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = exports = InvalidRoutineTypeError
|
@ -19,6 +19,14 @@ class State extends Injectable {
|
||||
async reverse() {
|
||||
throw new ImplementationError()
|
||||
}
|
||||
|
||||
failure_message() {
|
||||
throw new ImplementationError()
|
||||
}
|
||||
|
||||
check_message() {
|
||||
throw new ImplementationError()
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = exports = State
|
||||
|
@ -26,6 +26,14 @@ class DirectoryState extends State {
|
||||
async _path() {
|
||||
return this._host.get_path(this._config.path)
|
||||
}
|
||||
|
||||
failure_message() {
|
||||
return `The path "${this._config.path}" does not exist on host "${this._host.name}."`
|
||||
}
|
||||
|
||||
check_message() {
|
||||
return this.failure_message()
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = exports = DirectoryState
|
||||
|
@ -46,6 +46,14 @@ class DownloadState extends State {
|
||||
if ( !this._config.path ) throw new Error('Missing path config for DownloadState.')
|
||||
return this._host.get_path(this._config.path)
|
||||
}
|
||||
|
||||
failure_message() {
|
||||
return `The downloaded file "${this._config.path}" does not exist on host "${this._host.name}."`
|
||||
}
|
||||
|
||||
check_message() {
|
||||
return this.failure_message()
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = exports = DownloadState
|
||||
|
@ -31,6 +31,14 @@ class FileState extends State {
|
||||
if ( !this._config.path ) throw new Error('Missing path config for FileState.')
|
||||
return this._host.get_path(this._config.path)
|
||||
}
|
||||
|
||||
failure_message() {
|
||||
return `The file "${this._config.path}" does not exist on the host "${this._host.name}."`
|
||||
}
|
||||
|
||||
check_message() {
|
||||
return this.failure_message()
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = exports = FileState
|
||||
|
@ -39,6 +39,13 @@ class OwnerState extends State {
|
||||
return path
|
||||
}
|
||||
|
||||
failure_message() {
|
||||
return `The ownership state of the file "${this._config.path}" on host "${this._host.name}" is invalid.`
|
||||
}
|
||||
|
||||
check_message() {
|
||||
return this.failure_message()
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = exports = OwnerState
|
||||
|
@ -55,6 +55,14 @@ class PackState extends State {
|
||||
if ( !path.is_valid() ) throw new Error(`Invalid path for PathState: ${path}`)
|
||||
return path
|
||||
}
|
||||
|
||||
failure_message() {
|
||||
return `The archive "${this._config.destination}" does not exist on the host "${this._host.name}."`
|
||||
}
|
||||
|
||||
check_message() {
|
||||
return this.failure_message()
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = exports = PackState
|
||||
|
@ -34,6 +34,14 @@ class PermissionState extends State {
|
||||
if ( !path.is_valid() ) throw new Error(`Invalid path for PermissionState: ${path}`)
|
||||
return path
|
||||
}
|
||||
|
||||
failure_message() {
|
||||
return `The resource permissions for "${this._config.path}" on host "${this._host.name}" are invalid.`
|
||||
}
|
||||
|
||||
check_message() {
|
||||
return this.failure_message()
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = exports = PermissionState
|
||||
|
@ -58,6 +58,14 @@ class UnpackState extends State {
|
||||
if ( !path.is_directory() ) throw new Error(`Invalid extraction path. Must be a directory: ${path}`)
|
||||
return path
|
||||
}
|
||||
|
||||
failure_message() {
|
||||
return `The unpacked archive does not exist at the path "${this._config.destination}" on host "${this._host.name}."`
|
||||
}
|
||||
|
||||
check_message() {
|
||||
return this.failure_message()
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = exports = UnpackState
|
||||
|
@ -33,6 +33,13 @@ class CheckoutState extends AbstractGitState {
|
||||
}
|
||||
}
|
||||
|
||||
failure_message() {
|
||||
return `The Git repo at "${this._config.path}" on host "${this._host.name}" is not checked out to the correct ref (${this._config.target}).`
|
||||
}
|
||||
|
||||
check_message() {
|
||||
return this.failure_message()
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = exports = CheckoutState
|
||||
|
@ -21,6 +21,13 @@ class CloneState extends AbstractGitState {
|
||||
}
|
||||
}
|
||||
|
||||
failure_message() {
|
||||
return `Could not find the cloned Git repo at "${this._config.path}" on host "${this._host.name}."`
|
||||
}
|
||||
|
||||
check_message() {
|
||||
return this.failure_message()
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = exports = CloneState
|
||||
|
@ -26,6 +26,13 @@ class TagState extends AbstractGitState {
|
||||
}
|
||||
}
|
||||
|
||||
failure_message() {
|
||||
return `The tag "${this._config.tag}" does not exist in the Git repo "${this._config.path}" on host "${this._host.name}."`
|
||||
}
|
||||
|
||||
check_message() {
|
||||
return this.failure_message()
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = exports = TagState
|
||||
|
@ -23,6 +23,13 @@ class CommandState extends State {
|
||||
}
|
||||
}
|
||||
|
||||
failure_message() {
|
||||
return `Failed to execute the command "${this._config.cmd}" on host "${this._host.name}."`
|
||||
}
|
||||
|
||||
check_message() {
|
||||
return `The command check was not successful.`
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = exports = CommandState
|
||||
|
25
app/classes/state/os/IsAliveState.js
Normal file
25
app/classes/state/os/IsAliveState.js
Normal file
@ -0,0 +1,25 @@
|
||||
const State = require('../State')
|
||||
|
||||
class IsAliveState extends State {
|
||||
async apply() {
|
||||
throw new Error('IsAliveState cannot be applied. It is a check measure only.')
|
||||
}
|
||||
|
||||
async check() {
|
||||
return this._host.is_alive()
|
||||
}
|
||||
|
||||
async reverse() {
|
||||
throw new Error('IsAliveState cannot be reversed. It is a check measure only.')
|
||||
}
|
||||
|
||||
failure_message() {
|
||||
return `Unable to connect to host "${this._host.name}".`
|
||||
}
|
||||
|
||||
check_message() {
|
||||
return this.failure_message()
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = exports = IsAliveState
|
@ -19,6 +19,14 @@ class PackageAbsentState extends State {
|
||||
async apply() {
|
||||
return this._host.packages.uninstall(this._config.package)
|
||||
}
|
||||
|
||||
failure_message() {
|
||||
return `The package "${this._config.package}" still exists on host "${this._host.name}."`
|
||||
}
|
||||
|
||||
check_message() {
|
||||
return this.failure_message()
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = exports = PackageAbsentState
|
||||
|
@ -23,6 +23,14 @@ class PackageCacheClearedState extends State {
|
||||
async reverse() {
|
||||
this.output.warn(`Package cache cleared state does not currently support reversal. (Host: ${this._host.name})`)
|
||||
}
|
||||
|
||||
failure_message() {
|
||||
return `Failed to clear package cache on host "${this._host.name}."`
|
||||
}
|
||||
|
||||
check_message() {
|
||||
return `The package cache on host "${this._host.name}" has not been cleared.`
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = exports = PackageCacheClearedState
|
||||
|
@ -19,6 +19,14 @@ class PackageState extends State {
|
||||
async reverse() {
|
||||
return this._host.packages.uninstall(this._config.package)
|
||||
}
|
||||
|
||||
failure_message() {
|
||||
return `The package "${this._config.package}" is not installed on host "${this._host.name}."`
|
||||
}
|
||||
|
||||
check_message() {
|
||||
return this.failure_message()
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = exports = PackageState
|
||||
|
@ -23,6 +23,14 @@ class ServiceDaemonReloadState extends State {
|
||||
async reverse() {
|
||||
this.output.warn(`Service daemon reload state does not currently support reversal. (Host: ${this._host.name})`)
|
||||
}
|
||||
|
||||
failure_message() {
|
||||
return `Unable to reload service daemon on host "${this._host.name}."`
|
||||
}
|
||||
|
||||
check_message() {
|
||||
return `The service daemon on host "${this._host.name}" has not been reloaded.`
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = exports = ServiceDaemonReloadState
|
||||
|
@ -24,6 +24,14 @@ class ServiceRestartState extends State {
|
||||
async reverse() {
|
||||
this.output.warn(`Service restart state does not currently support reversal. (Host: ${this._host.name})`)
|
||||
}
|
||||
|
||||
failure_message() {
|
||||
return `Unable to restart service "${this._config.service}" on host "${this._host.name}."`
|
||||
}
|
||||
|
||||
check_message() {
|
||||
return `The service "${this._config.service}" on host "${this._host.name}" has not been restarted.`
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = exports = ServiceRestartState
|
||||
|
@ -24,6 +24,15 @@ class ServiceState extends State {
|
||||
const services = Array.isArray(this._config.service) ? this._config.service : [this._config.service]
|
||||
return this._host.services.stop(...services)
|
||||
}
|
||||
|
||||
failure_message() {
|
||||
const services = Array.isArray(this._config.service) ? this._config.service : [this._config.service]
|
||||
return `The service(s) ${services.join(', ')} do not all exist on the host "${this._host.name}."`
|
||||
}
|
||||
|
||||
check_message() {
|
||||
return this.failure_message()
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = exports = ServiceState
|
||||
|
@ -24,6 +24,14 @@ class ServiceStoppedState extends State {
|
||||
const services = Array.isArray(this._config.service) ? this._config.service : [this._config.service]
|
||||
return this._host.services.stop(...services)
|
||||
}
|
||||
|
||||
failure_message() {
|
||||
return `Unable to stop service "${this._config.service}" on host "${this._host.name}."`
|
||||
}
|
||||
|
||||
check_message() {
|
||||
return `The service "${this._config.service}" on host "${this._host.name}" has not been stopped.`
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = exports = ServiceStoppedState
|
||||
|
@ -24,6 +24,14 @@ class UpdateState extends State {
|
||||
async reverse() {
|
||||
this.output.warn(`Update state does not currently support reversing package updates. (Host: ${this._host.name})`)
|
||||
}
|
||||
|
||||
failure_message() {
|
||||
return `Unable to update packages on host "${this._host.name}."`
|
||||
}
|
||||
|
||||
check_message() {
|
||||
return `There are package updates pending on the host "${this._host.name}."`
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = exports = UpdateState
|
||||
|
30
app/services/routines.service.js
Normal file
30
app/services/routines.service.js
Normal file
@ -0,0 +1,30 @@
|
||||
const { Service } = require('flitter-di')
|
||||
const Routine = require('../classes/routine/Routine')
|
||||
|
||||
/*
|
||||
* routines Service
|
||||
* -------------------------------------------------------------
|
||||
* This is a service file that will be made available through Flitter's
|
||||
* dependency injector to the rest of the application based on its given
|
||||
* canonical name.
|
||||
*
|
||||
* e.g. app.di().service("routines")
|
||||
*/
|
||||
class RoutinesService extends Service {
|
||||
static get services() {
|
||||
return [...super.services, 'app', 'configs', 'hosts']
|
||||
}
|
||||
|
||||
async get(name) {
|
||||
this.app.make(Routine)
|
||||
const config = this.configs.get(`routines:${name}`)
|
||||
const hosts = {}
|
||||
for ( const host_name of config.hosts ) {
|
||||
hosts[host_name] = await this.hosts.get(host_name)
|
||||
}
|
||||
|
||||
return new Routine(hosts, config, config.type)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = exports = RoutinesService
|
@ -27,6 +27,7 @@ class StatesService extends Service {
|
||||
'git.tag': require('../classes/state/git/TagState'),
|
||||
|
||||
'os.cmd': require('../classes/state/os/CommandState'),
|
||||
'os.alive': require('../classes/state/os/IsAliveState'),
|
||||
|
||||
'package.present': require('../classes/state/os/PackageState'),
|
||||
'package.absent': require('../classes/state/os/PackageAbsentState'),
|
||||
@ -45,13 +46,18 @@ class StatesService extends Service {
|
||||
return [...super.services, 'app', 'configs']
|
||||
}
|
||||
|
||||
map() {
|
||||
return this.constructor.#state_map
|
||||
}
|
||||
|
||||
from_config(host, state_config) {
|
||||
const type = state_config.type
|
||||
state_config = {...state_config}
|
||||
delete state_config.type
|
||||
|
||||
const StepClass = this.constructor.#state_map[type]
|
||||
this.app.di().make(StepClass)
|
||||
if ( !StepClass ) throw new Error(`Invalid or unknown step type: ${type}`)
|
||||
this.app.make(StepClass)
|
||||
|
||||
return new StepClass(host, state_config)
|
||||
}
|
||||
|
@ -27,6 +27,22 @@ const hosts = {
|
||||
},
|
||||
},
|
||||
|
||||
edge: {
|
||||
type: 'ssh',
|
||||
ssh_params: {
|
||||
port: 60022,
|
||||
username: 'root',
|
||||
host: 'edge.infrastructure',
|
||||
key_file: '/home/garrettmills/.ssh/id_rsa',
|
||||
},
|
||||
packages: {
|
||||
type: 'apt',
|
||||
},
|
||||
services: {
|
||||
type: 'systemd',
|
||||
},
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
module.exports = exports = hosts
|
||||
|
16
config/notify.config.js
Normal file
16
config/notify.config.js
Normal file
@ -0,0 +1,16 @@
|
||||
// This is the configuration for the Flitter Gotify wrapper service, 'notify'.
|
||||
const notify = {
|
||||
// URL to the Gotify host (e.g. https://my-gotify.server.url/)
|
||||
host: env('GOTIFY_HOST'),
|
||||
|
||||
// collection of notification channel groups
|
||||
groups: {
|
||||
// default group. You can specify as many groups as you want.
|
||||
// Each group should be an array of Gotify app keys.
|
||||
default: [
|
||||
env('GOTIFY_DEFAULT_APP_KEY'),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = exports = notify
|
20
config/routines/login.config.js
Normal file
20
config/routines/login.config.js
Normal file
@ -0,0 +1,20 @@
|
||||
const login_config = {
|
||||
type: 'checks',
|
||||
hosts: ['core', 'localhost', 'edge'],
|
||||
steps: [
|
||||
{
|
||||
type: 'os.alive',
|
||||
host: 'core',
|
||||
},
|
||||
{
|
||||
type: 'os.alive',
|
||||
host: 'edge',
|
||||
},
|
||||
{
|
||||
type: 'os.alive',
|
||||
host: 'localhost',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
module.exports = exports = login_config
|
18
config/routines/tmpdir.config.js
Normal file
18
config/routines/tmpdir.config.js
Normal file
@ -0,0 +1,18 @@
|
||||
const tmpdir_config = {
|
||||
type: 'checks',
|
||||
hosts: ['core', 'localhost'],
|
||||
steps: [
|
||||
{
|
||||
type: 'fs.directory',
|
||||
host: 'core',
|
||||
path: '/tmp/glmdev',
|
||||
},
|
||||
{
|
||||
type: 'fs.directory',
|
||||
host: 'localhost',
|
||||
path: '/tmp/glmdev',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
module.exports = exports = tmpdir_config
|
16
config/routines/updates.config.js
Normal file
16
config/routines/updates.config.js
Normal file
@ -0,0 +1,16 @@
|
||||
const updates_config = {
|
||||
type: 'checks',
|
||||
hosts: ['core', 'localhost'],
|
||||
steps: [
|
||||
{
|
||||
type: 'package.updates',
|
||||
host: 'core',
|
||||
},
|
||||
{
|
||||
type: 'package.updates',
|
||||
host: 'localhost',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
module.exports = exports = updates_config
|
@ -24,6 +24,7 @@
|
||||
"flitter-di": "^0.4.1",
|
||||
"flitter-flap": "^0.5.2",
|
||||
"flitter-forms": "^0.8.0",
|
||||
"flitter-gotify": "^0.1.0",
|
||||
"flitter-upload": "^0.7.6",
|
||||
"libflitter": "^0.46.8",
|
||||
"moment": "^2.24.0",
|
||||
|
Loading…
Reference in New Issue
Block a user