lcpex rewrite - alpha

master Rex-2.0a
phanes 2023-02-15 18:44:21 -05:00
parent bb85754dc0
commit 7ed6e13fa5
82 changed files with 4510 additions and 2592 deletions

View File

@ -1,8 +1,6 @@
cmake_minimum_required(VERSION 3.5)
cmake_minimum_required(VERSION 3.23)
project(rex)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -std=c++1z -O0 -DDEBUG=1")
set(SOURCE_FILES Rex.cpp src/loaders/abstract/loaders.cpp src/loaders/abstract/loaders.h src/json/jsoncpp.cpp src/loaders/low_level/JSON_Loader.cpp src/loaders/low_level/JSON_Loader.h src/loaders/misc/helpers.cpp src/loaders/misc/helpers.h src/loaders/abstract/Suite.cpp src/loaders/abstract/Suite.h src/loaders/abstract/Plan.cpp src/loaders/abstract/Plan.h src/loaders/abstract/Conf.cpp src/loaders/abstract/Conf.h src/loaders/abstract/Unit.cpp src/loaders/abstract/Unit.h src/loaders/abstract/Task.cpp src/loaders/abstract/Task.h src/Sproc/Sproc.cpp src/Sproc/Sproc.h src/Logger/Logger.cpp src/Logger/Logger.h)
set(CMAKE_CXX_STANDARD 14)
add_executable(rex ${SOURCE_FILES})
add_executable(rex Rex.cpp src/json_support/jsoncpp/json.h src/json_support/jsoncpp/json-forwards.h src/json_support/jsoncpp/jsoncpp.cpp src/logger/Logger.cpp src/logger/Logger.h src/json_support/JSON.cpp src/json_support/JSON.h src/misc/helpers.cpp src/misc/helpers.h src/config/Config.cpp src/config/Config.h src/suite/Suite.cpp src/suite/Suite.h src/suite/Unit.cpp src/suite/Unit.h src/shells/shells.cpp src/shells/shells.h src/plan/Plan.cpp src/plan/Plan.h src/plan/Task.cpp src/plan/Task.h src/lcpex/liblcpex.h src/lcpex/liblcpex.cpp src/lcpex/vpty/libclpex_tty.h src/lcpex/vpty/libclpex_tty.cpp src/lcpex/Contexts.h src/lcpex/Contexts.cpp src/lcpex/helpers.h src/lcpex/string_expansion/string_expansion.h src/lcpex/string_expansion/string_expansion.cpp src/lcpex/vpty/pty_fork_mod/pty_fork.h src/lcpex/vpty/pty_fork_mod/pty_fork.cpp src/lcpex/vpty/pty_fork_mod/pty_master_open.h src/lcpex/vpty/pty_fork_mod/pty_master_open.cpp src/lcpex/vpty/pty_fork_mod/tty_functions.h src/lcpex/vpty/pty_fork_mod/tty_functions.cpp )

106
Rex.cpp
View File

@ -1,54 +1,61 @@
/*
Rex - A configuration management and workflow automation tool that
compiles and runs in minimal environments.
© SILO GROUP and Chris Punches, 2020.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include <iostream>
#include <unistd.h>
#include <getopt.h>
#include "src/loaders/abstract/loaders.h"
#include "src/Logger/Logger.h"
#include "src/logger/Logger.h"
#include "src/config/Config.h"
#include "src/suite/Suite.h"
#include "src/plan/Plan.h"
void version_info()
{
std::cout << "pre-release alpha" << std::endl;
}
// helper function to print out commandline arguments for print_usage()
void print_arg(const char *short_opt, const char *long_opt, const char *desc)
{
fprintf(stderr, " %-3s %-25s %s\n", short_opt, long_opt, desc);
}
void print_section_header(const std::string &header_title)
{
fprintf(stderr, "\n%s:\n", header_title.c_str());
}
void print_usage()
{
// commandline switches:
// -h help OPTIONAL
// -v verbose OPTIONAL
// -c CONFIG_FILE REQUIRED
// -p PLAN_FILE REQUIRED
fprintf(stderr, "\nUsage:\n\trex [ -h | --help ] [ -v | --verbose ] ( ( ( -c | --config ) CONFIG_PATH ) ( -p | plan ) PLAN_PATH ) )\n");
fprintf( stderr, "\nUsage:\n\trex [ -h | --help ] [ -v | --verbose ] ( ( ( -c | --config ) CONFIG_PATH ) ( -p | plan ) PLAN_PATH ) )\n" );
print_section_header("Optional Arguments");
print_arg( "-h", "--help", "This usage screen. Mutually exclusive to all other options.");
print_arg( "-v", "--verbose", "Sets verbose output. Generally more than you want to see.");
print_arg( "-i", "--version_info", "Prints version information and exits. Mutually exclusive to all other options.");
fprintf( stderr, "\nOptional Arguments:\n");
fprintf( stderr, "\t-h | --help\n\t\tThis usage screen. Mutually exclusive to all other options.\n");
fprintf( stderr, "\t-v | --verbose\n\t\tSets verbose output. Generally more than you want to see.\n");
fprintf( stderr, "\nRequired Arguments:\n");
fprintf( stderr, "\t-c | --config\n\t\tSupply the directory path for the configuration file.\n");
fprintf( stderr, "\t-p | --plan\n\t\tSupply the directory path for the plan file to execute.\n\n");
print_section_header("Required Arguments");
print_arg( "-c", "--config", "Supply the path for the configuration file.");
print_arg( "-p", "--plan", "Supply the path for the plan file to execute.");
fprintf(stderr, "\n");
}
int main( int argc, char * argv[] )
{
// default verbosity setting
@ -128,12 +135,18 @@ int main( int argc, char * argv[] )
break;
} // end switch
} // end opts while
if ( version_flag ) {
version_info();
exit(0);
}
// if the user wants the help screen, just show it and leave
if ( (help_flag) | (! config_flag) | (! plan_flag) )
{
print_usage();
exit( 0 );
}
// if the user supplied no config file, there's nothing to do but teach the user how to use this tool
if (! config_flag ) {
std::cerr << "NOT SUPPLIED: CONFIG_PATH" << std::endl;
@ -149,13 +162,6 @@ int main( int argc, char * argv[] )
interpolate( config_path );
interpolate( plan_path );
// if the user wants the help screen, just show it and leave
if ( (help_flag) | (! config_flag) | (! plan_flag) )
{
print_usage();
exit( 0 );
}
// default logging level
int L_LEVEL = E_INFO;
@ -168,52 +174,38 @@ int main( int argc, char * argv[] )
// the main scope logger
Logger slog = Logger( L_LEVEL, "_main_" );
slog.log( E_INFO, "* Initialising Logging...");
slog.log_task( E_DEBUG, "INIT", "Logging initialised." );
// configuration object that reads from config_path
Conf configuration = Conf( config_path, L_LEVEL );
slog.log_task(E_DEBUG, "INIT", "Configuration initialised.");
// check if context override is set in the config file
if ( configuration.has_context_override() )
{
// if so, set the CWD.
chdir( configuration.get_execution_context().c_str() );
slog.log( E_DEBUG, "* Setting execution context: " + get_working_path() );
}
// load the paths to definitions of units.
std::string unit_definitions_path = configuration.get_units_path();
// The Rex Paradigm:
// - A Suite is made up of Units.
// - A Unit is a definition of an executable along with various options surrounding its context and behaviour.
// - A Plan is made up of Tasks.
// - A Unit becomes a Task when it is added to a Plan.
// initialise an empty suite (unit definitions library)
slog.log_task( E_DEBUG, "SUITE_INIT", "Initialising Suite...");
Suite available_definitions = Suite( L_LEVEL );
// load units into suite
slog.log_task( E_INFO, "LOAD", "Loading all actionable Units into Suite..." );
available_definitions.load_units_file( unit_definitions_path );
// A Plan contains what units are executed and a Suite contains the definitions of those units.
std::string plan_file = plan_path;
// load the filepaths to definitions of a plan and definitions of units.
std::string definitions_file = configuration.get_units_path();
// initialise an empty suite (unit definitions library)
slog.log( E_DEBUG, "* Initialising Suite...");
Suite available_definitions = Suite( L_LEVEL );
// load units into suite
slog.log( E_INFO, "* Loading all actionable Units into Suite..." );
available_definitions.load_units_file( definitions_file );
// initialise an empty plan
slog.log( E_DEBUG, "* Initialising Plan..." );
slog.log_task( E_DEBUG, "PLAN_INIT", "Initialising Plan..." );
Plan plan = Plan( &configuration, L_LEVEL );
// load the plan the user supplied
slog.log( E_INFO, "* Loading Plan...");
plan.load_plan_file( plan_file );
// ingest the suitable Tasks from the Suite into the Plan
slog.log( E_INFO, "* Loading planned Tasks from Suite to Plan." );
slog.log_task( E_INFO, "LOAD", "Loading planned Tasks from Suite to Plan." );
plan.load_definitions( available_definitions );
slog.log( E_INFO, "* Ready to execute all actionable Tasks in Plan." );
slog.log_task( E_INFO, "main", "Ready to execute all actionable Tasks in Plan." );
try
{

33
doc/Config.md Normal file
View File

@ -0,0 +1,33 @@
# Config
The Rex configuration is as a json object in a file that Rex is pointed to as a commandline argument.
In that file's json object, there is a field named "config", whose
properties define the configuration of Rex.
There are currently 4 parameters that can be configured in the configuration file:
1. `project_root`: The root directory of the project.
2. `units_path`: The path to the units Library.
3. `logs_path`: The path to the logs directory.
4. `config_version`: The version of the configuration file.
## Example
```json
{
"config": {
"project_root": "/home/user/project",
"units_path": "/home/user/project/units",
"logs_path": "/home/user/project/logs",
"config_version": 1
}
}
```
There are some things to be aware of when setting the configuration file:
1. The configuration file must be a valid json object.
2. The configuration file must have a field named "config", whose properties define the configuration of Rex.
3. All values for paths in this file are relative to the `project_root` path.
4. The `logs_path` location will be created if it does not exist when Rex begins to execute.

78
doc/SUMMARY.md Normal file
View File

@ -0,0 +1,78 @@
# Rex
Rex executes chains of automations.
Those automations are generally user-defined executables, represented in a
project structure as json objects.
Rex, when executing, is supplied a config file and a plan.
## Config
The config file contains a json object that defines where the resources Rex
will operate with are located on the filesystem, and the general project
structure for the plan being executed.
## Plans
A `plan` is central to the operation of rex. It is a file containing a json
object that defines which `units` to execute from the `units library`. Once
a unit is specified in a `plan`, it is referred to as a `task`.
It should be noted that the config file and the plan file are not required
to be separate files, but this is generally easier to manage from a project
structure standpoint.
## Library / Units
The `units library` is generally where these automations are defined. This
is a directory containing files that end with the `.units` suffix. Each of
those files is a json object with specifications for:
1. What is being executed.
2. How it is being executed.
3. The context around which it is executed.
4. An identifier by which the execution is referenced.
Exact specifications will be enumerated in a later section.
This allows these automations to executed with very tight control of the environment,
current working directory, the execution process' owner and group context, whether a
pty is used etc.
Among other things, the path for the units library is defined in the config file
supplied at the command line to rex.
For reasons later explained, the presence of a unit definition in the units library
does NOT mean that rex will execute that unit. It merely makes a unit available to
be included as a task in a plan.
## Self-Healing Workflows
Rex introduces self-healing workflows.
### Model A
1. Write a script that does a thing.
2. Write a script that checks if that thing is done.
3. Set up your check script as a target in a unit.
4. Set up your script that does the thing as that unit's rectifier.
5. Turn on the rectify pattern in the unit definition.
### Model B
Or, if you want a flow that's more simple:
1. Write a script that does a thing.
2. Write a script that fixes any environmental conditions that prevent that thing from being done.
3. Set up your target script to be the script that does the thing.
4. Set up your rectifier script to be the script that fixes the bad condition.
5. Turn on the rectify pattern in the unit definition.
## Traditional Workflows
You don't need a dual mode of automation (though it is highly recommended). You can just do this:
1. Write a script that does a thing.
2. Set the user, group to run as, well as the shell that should be used to execute within.
3. Set the environment file to a file to be sourced for that shell containing all of your environment definitions for your automation.
4. Turn off the rectify pattern.
5. Repeat for every step.
As you can see, for small automations, it's often going to be more desirable to just write a shell script, but for very large efforts spanning many subsystems or components, you may want more control -- that's really where Rex comes in.

View File

Before

Width:  |  Height:  |  Size: 94 KiB

After

Width:  |  Height:  |  Size: 94 KiB

View File

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View File

View File

@ -0,0 +1,2 @@
lcpex: setgid failed: Operation not permitted
lcpex: Aborting: Setting GID failed: bagira/bagira

View File

@ -0,0 +1 @@
/usr/bin/bash: line 1: /home/phanes/development/internal/rex-rewrite/sample/environments/rex.variables: Permission denied

View File

@ -0,0 +1 @@
/usr/bin/bash: line 1: /home/phanes/development/internal/rex-rewrite/sample/environments/rex.variables: Permission denied

5
sample/plans/test.plan Normal file
View File

@ -0,0 +1,5 @@
{
"plan": [
{ "name": "independent test 1", "dependencies": [ null ] }
]
}

9
sample/rex.config Normal file
View File

@ -0,0 +1,9 @@
{
"config": {
"project_root": "/home/phanes/development/internal/rex-rewrite/sample",
"units_path": "units/",
"logs_path": "logs/",
"shells_path": "shells/shells.definitions",
"config_version": "5"
}
}

View File

@ -0,0 +1,16 @@
{
"shells": [
{
"name": "bash",
"path": "/usr/bin/bash",
"execution_arg": "-c",
"source_cmd": "source"
},
{
"name": "ksh",
"path": "/usr/bin/ksh",
"execution_arg": "-c",
"source_cmd": "source"
}
]
}

BIN
sample/units/.new.units.swp Normal file

Binary file not shown.

22
sample/units/new.units Normal file
View File

@ -0,0 +1,22 @@
{
"units": [
{
"name": "independent test 1",
"target": "components/independent_test_1.bash --ls -s --arg",
"is_shell_command": true,
"shell_definition": "bash",
"force_pty": true,
"set_working_directory": false,
"working_directory": "",
"rectify": false,
"rectifier": "echo rectifier executed",
"active": true,
"required": true,
"set_user_context": true,
"user": "bagira",
"group": "bagira",
"supply_environment": true,
"environment": "environments/rex.variables"
}
]
}

View File

@ -1,446 +0,0 @@
/*
Rex - A configuration management and workflow automation tool that
compiles and runs in minimal environments.
© SILO GROUP and Chris Punches, 2020.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "Sproc.h"
#include "sys/stat.h"
#define PARENT default
enum PIPE_FILE_DESCRIPTORS {
READ_END = 0,
WRITE_END = 1
};
enum READ_RESULTS {
READ_EOF = 0,
READ_PIPEOPEN_O_NONBLOCK = -1
};
enum FORK_STATES {
FORK_FAILURE = -1,
CHILD = 0
};
/* ------------------
* HELPERS
* ------------------ */
// converts username to UID
// returns false on failure
int username_to_uid( std::string username, int & uid )
{
// assume failure unless proven otherwise
int r_code = false;
struct passwd * pw;
if ( ( pw = getpwnam( username.c_str() ) ) != NULL )
{
// successful user lookup
r_code = true;
uid = pw->pw_uid;
} else {
// failed lookup, do nothing, assumed failure
}
return r_code;
};
// converts group name to GID
// returns false on failure
int groupname_to_gid( std::string groupname, int & gid )
{
int r_code = false;
struct group * gp;
if ( ( gp = getgrnam( groupname.c_str() ) ) != NULL )
{
r_code = true;
gid = gp->gr_gid;
} else {
// failed lookup, do nothing, assumed failure
}
return r_code;
}
// teebuf constructor
teebuf::teebuf(std::streambuf *sb1, std::streambuf *sb2): sb1(sb1), sb2(sb2)
{}
// teebuf overflow method
int teebuf::overflow( int c )
{
if (c == EOF)
{
return !EOF;
}
else
{
int const r1 = sb1->sputc(c);
int const r2 = sb2->sputc(c);
return r1 == EOF || r2 == EOF ? EOF : c;
}
}
// teebuf sync method
int teebuf::sync()
{
int const r1 = sb1->pubsync();
int const r2 = sb2->pubsync();
return r1 == 0 && r2 == 0 ? 0 : -1;
}
// teestream impl constructor
teestream::teestream(std::ostream &o1, std::ostream &o2) : std::ostream( &tbuf), tbuf( o1.rdbuf(), o2.rdbuf() )
{}
// SET PROCESS TO A CERTAIN IDENTITY CONTEXT
int set_identity_context( std::string task_name, std::string user_name, std::string group_name, Logger slog ) {
// the UID and GID for the username and groupname provided for context setting
int context_user_id;
int context_group_id;
// show the user something usable in debug mode
slog.log( E_DEBUG, "[ '" + task_name + "' ] Attempt: Running as user '" + user_name + "'.");
slog.log( E_DEBUG, "[ '" + task_name + "' ] Attempt: Running as group_name '" + group_name + "'.");
// convert username to UID
if ( username_to_uid(user_name, context_user_id ) )
{
slog.log( E_DEBUG, "[ '" + task_name + "' ] UID of '" + user_name + "' is '" + std::to_string(context_user_id ) + "'." );
} else {
slog.log( E_FATAL, "[ '" + task_name + "' ] Failed to look up UID for '" + user_name + "'.");
return SPROC_RETURN_CODES::UID_NOT_FOUND;
}
// convert group name to GID
if ( groupname_to_gid(group_name, context_group_id ) )
{
slog.log( E_DEBUG, "[ '" + task_name + "' ] GID of '" + group_name + "' is '" + std::to_string(context_group_id ) + "'." );
} else {
slog.log( E_FATAL, "[ '" + task_name + "' ] Failed to look up GID for '" + group_name + "'.");
return SPROC_RETURN_CODES::GID_NOT_FOUND;
}
if (setgid(context_group_id) == 0) {
slog.log(E_DEBUG,
"[ '" + task_name + "' ] Successfully set GID to '" + std::to_string(context_group_id) + "' (" +
group_name + ").");
} else {
slog.log(E_FATAL, "[ '" + task_name + "' ] Failed to set GID. Panicking.");
return SPROC_RETURN_CODES::SET_GID_FAILED;
}
if (setuid(context_user_id) == 0) {
slog.log(E_DEBUG,
"[ '" + task_name + "' ] Successfully set UID to '" + std::to_string(context_user_id) + "' (" +
user_name + ").");
} else {
slog.log(E_FATAL, "[ '" + task_name + "' ] Failed to set UID. Panicking.");
return SPROC_RETURN_CODES::SET_UID_FAILED;
}
return SPROC_RETURN_CODES::SUCCESS;
}
/// Sproc::execute
///
/// \param input - The commandline input to execute.
/// \return - The return code of the execution of input in the calling shell.
int Sproc::execute(std::string shell, std::string environment_file, std::string user_name, std::string group_name, std::string command, int LOG_LEVEL, std::string task_name, bool log_to_file, std::string logs_dir )
{
// the logger
Logger slog = Logger( LOG_LEVEL, "_sproc" );
// if you get this return value, it's an issue with this method and not your
// called executable.
int exit_code_raw = SPROC_RETURN_CODES::EXEC_FAILURE_GENERAL;
// An explanation is due here:
// We want to log the STDOUT and STDERR of the child process, while still displaying them in the parent, in a way
// that does not interfere with, for example libcurses compatibility.
// To simplify the handling of I/O, we will "tee" STDOUT and STDERR of the parent to respective log files.
// Then fork(), and exec() the command to execute in the child, and link its STDOUT/STDERR to the parents' in
// realtime.
// Since the parent has a Tee between STDOUT/STDOUT_LOG and another between STDERR/STDERR_LOG, simply piping the
// child STDOUT/STDERR to the parent STDOUT/STDERR should simplify I/O redirection happening here without
// potentially corrupting user interaction with TUIs in the processes. This should give us our log and our output
// in as hands off a way as possible with as few assumptions as possible, while still doing this in a somewhat C++-y
// way.
if (! is_dir( logs_dir ) ) {
int check = mkdir( logs_dir.c_str(), 0777 );
if (! check ) {
slog.log( E_FATAL, "Sprocket couldn't create the logs parent directory." );
}
}
std::string timestamp = get_8601();
std::string contained_dir = logs_dir + "/" + task_name;
if (! is_dir( contained_dir ) ) {
int check = mkdir( contained_dir.c_str(), 0777 );
if (! check ) {
slog.log( E_FATAL, "Sprocket couldn't create the instance log directory.");
}
}
// set up the "Tee" with the parent
std::string child_stdout_log_path = contained_dir + "/" + timestamp + ".stdout.log";
std::string child_stderr_log_path = contained_dir + "/" + timestamp + ".stderr.log";
std::ofstream stdout_log;
std::ofstream stderr_log;
stdout_log.open( child_stdout_log_path.c_str(), std::ofstream::out | std::ofstream::app );
stderr_log.open( child_stderr_log_path.c_str(), std::ofstream::out | std::ofstream::app );
// avoid cyclic dependencies between stdout and tee_out
std::ostream tmp_stdout( std::cout.rdbuf() );
std::ostream tmp_stderr( std::cerr.rdbuf() );
// writing to this ostream derivative will write to stdout log file and std::cout
teestream tee_out(tmp_stdout, stdout_log);
teestream tee_err(tmp_stderr, stderr_log);
// pop the cout/cerr buffers to the appropriate Tees' buffers
// These cause a segfault when used with the I/O redirection happening around fork, pipe, dup2, execl...
//std::cout.rdbuf( tee_out.rdbuf() );
//std::cerr.rdbuf( tee_err.rdbuf() );
// ....and I don't know why.
// build the command to execute in the shell
std::string sourcer = ". " + environment_file + " && " + command;
// Show the user a debug print of what is going to be executed in the shell.
slog.log(E_INFO, "[ '" + task_name + "' ] Shell call for loading: ``" + sourcer + "``.");
// file descriptors for parent/child i/o
int child_stdout_pipe[2];
int child_stderr_pipe[2];
// no longer really needed, but may be handy for more verbosity settings later
//slog.log( E_DEBUG, "[ '" + task_name + "' ] STDIN/STDOUT/STDERR file descriptors created." );
// man 3 pipe
if (pipe(child_stdout_pipe) == -1 ) {
slog.log(E_FATAL, "[ '" + task_name + "' ] STDOUT PIPE FAILED");
return SPROC_RETURN_CODES::PIPE_FAILED;
} else {
slog.log(E_DEBUG, "[ '" + task_name + "' ] STDOUT Piped to Log.");
}
// man 3 pipe
if (pipe(child_stderr_pipe) == -1 ) {
slog.log(E_FATAL, "[ '" + task_name + "' ] STDERR PIPE FAILED");
return SPROC_RETURN_CODES::PIPE_FAILED;
} else {
slog.log(E_DEBUG, "[ '" + task_name + "' ] STDERR Piped to Log.");
}
if (fcntl(child_stdout_pipe[READ_END], F_SETFD, FD_CLOEXEC) == -1) {
perror("fcntl");
exit(1);
}
if (fcntl(child_stdout_pipe[WRITE_END], F_SETFD, FD_CLOEXEC) == -1) {
perror("fcntl");
exit(1);
}
if (fcntl(child_stderr_pipe[READ_END], F_SETFD, FD_CLOEXEC) == -1) {
perror("fcntl");
exit(1);
}
if (fcntl(child_stderr_pipe[WRITE_END], F_SETFD, FD_CLOEXEC) == -1) {
perror("fcntl");
exit(1);
}
// fork a process
pid_t pid = fork();
slog.log( E_DEBUG, "[ '" + task_name + "' ] Process forked. Reporting. (PID: " + std::to_string(pid) + ")" );
switch ( pid ) {
case FORK_STATES::FORK_FAILURE:
{
// fork failed
slog.log(E_FATAL, "[ '" + task_name + "' ] Fork Failed.");
exit( FORK_FAILED );
break;
}
case FORK_STATES::CHILD:
{
// enter child process
slog.log(E_DEBUG, "[ '" + task_name + "' ] Entering child process.");
while ((dup2(child_stdout_pipe[WRITE_END], STDOUT_FILENO) == -1) && (errno == EINTR)) {}
while ((dup2(child_stderr_pipe[WRITE_END], STDERR_FILENO) == -1) && (errno == EINTR)) {}
close( child_stdout_pipe[WRITE_END] );
close( child_stdout_pipe[READ_END] );
close( child_stderr_pipe[WRITE_END] );
close( child_stderr_pipe[READ_END] );
slog.log(E_INFO, "[ '" + task_name + "' ] TEE Logging enabled.");
//slog.log(E_DEBUG, "[ '" + task_name + "' ] DUP2: child_*_pipe[1]->STD*_FILENO");
// set identity context
// set gid and uid
int context_status = set_identity_context(task_name, user_name, group_name, slog);
if (!(context_status)) {
slog.log(E_FATAL, "[ '" + task_name + "' ] Identity context set failed.");
return context_status;
} else {
slog.log( E_INFO, "[ '" + task_name + "' ] Identity context set as user '" + user_name + "' and group '" + group_name + "'." );
}
slog.log( E_DEBUG, "[ '" + task_name + "' ] EXECL_CALL_PARAM: " + sourcer.c_str() );
slog.log( E_DEBUG, "[ '" + task_name + "' ] CWD: " + get_current_dir_name() );
// execute our big nasty thing
// int ret = execlp( shell.c_str(), shell.c_str(), "-c", sourcer.c_str(), (char*)NULL );
int ret = execl(shell.c_str(), shell.c_str(), "-c", sourcer.c_str(), (char*)NULL );
// print something useful to debug with if execl fails
slog.log(E_FATAL, "[ '" + task_name + "' ] ret code: " + std::to_string(ret) + "; errno: " + strerror(errno));
// exit child -- if this is executing, you've had a failure
exit(exit_code_raw);
}
PARENT:
{
// enter the parent process
close(child_stdout_pipe[WRITE_END]);
close(child_stderr_pipe[WRITE_END]);
// buffers for reading from child fd's
char stdout_buf[4096] = {0};
char stderr_buf[4096] = {0};
// will contain a set of file descriptors to monitor representing stdout and stderr of the child process
fd_set readfds;
// loop completion flags
bool set_stdout_break = false;
bool set_stderr_break = false;
// read from fd until child completes -- signaled by stdout_break and stderr_break flags
while ((! set_stderr_break ) or (! set_stdout_break)) {
// clear it out to make sure it's clean
FD_ZERO( & readfds );
// add child stdout and stderr pipes (read end)
FD_SET( child_stdout_pipe[READ_END], & readfds );
FD_SET( child_stderr_pipe[READ_END], & readfds );
// for some reason select needs the highest number of the fd +1 of its own input
int highest_fd = child_stderr_pipe[READ_END] > child_stdout_pipe[READ_END] ? child_stderr_pipe[READ_END] : child_stdout_pipe[READ_END];
// wait for one of the fd's to become readable
if ( select( highest_fd + 1, &readfds, NULL, NULL, NULL ) >= 0 )
{ // can read any
if ( FD_ISSET( child_stdout_pipe[READ_END], &readfds ) )
{ // can read child stdout pipe
// read and return the byte size of what was read
int stdout_count = read(child_stdout_pipe[READ_END], stdout_buf, sizeof(stdout_buf) - 1);
switch(stdout_count) { // switch on the count size to allow for error return handling
case READ_PIPEOPEN_O_NONBLOCK:
if ( errno == EINTR ) {
continue;
} else {
slog.log(E_FATAL, "Pipe reading issue with child STDOUT.");
exit( PIPE_FAILED2 );
}
case READ_EOF:
// signal that STDOUT is complete
set_stdout_break = true;
break;
default:
if ( log_to_file ) {
tee_out.write( stdout_buf, stdout_count );
tee_out.flush();
} else {
std::cout.write( stdout_buf, stdout_count );
std::cout.flush();
}
// clear the buffer to prevent artifacts from previous loop
memset( &stdout_buf[0], 0, sizeof( stdout_buf ) -1 );
}
}
if ( FD_ISSET( child_stderr_pipe[READ_END], & readfds ) ) {
// can read child stderr pipe
// so do so
// read and return the byte size of what was read
int stderr_count = read( child_stderr_pipe[READ_END], stderr_buf, sizeof( stderr_buf ) -1 );
// switch on the count size to allow for error return handling
switch( stderr_count ) {
case READ_RESULTS::READ_PIPEOPEN_O_NONBLOCK:
if ( errno == EINTR ) {
continue;
} else {
perror( "read stderr" );
slog.log( E_FATAL, "Pipe reading issue with child STDERR." );
exit( PIPE_FAILED3 );
}
case READ_RESULTS::READ_EOF:
// let the loop know the STDERR criteria has been met
set_stderr_break = true;
// continue the loop
continue;
default:
// write the buffer contents to the STDERR Tee
tee_err.write( stderr_buf, stderr_count );
// flush the TEE
tee_err.flush();
// clear the buffer to prevent artifacts from previous loop
memset( &stderr_buf[0], 0, sizeof( stderr_buf ) -1 );
}
}
} else {
// select error, fatal, throw
slog.log( E_FATAL, "Fatal error, Unknown.");
} // end select/if
}
close( child_stderr_pipe[READ_END] );
close( child_stdout_pipe[READ_END] );
// clean up Tee
stdout_log.close();
stderr_log.close();
// wait for the child to exit
while ( ( pid = waitpid(pid, &exit_code_raw, 0 ) ) == -1 ) {}
}
}
return WEXITSTATUS( exit_code_raw );
}

View File

@ -1,97 +0,0 @@
/*
Rex - A configuration management and workflow automation tool that
compiles and runs in minimal environments.
© SILO GROUP and Chris Punches, 2020.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef REX_SPROCKET_H
#define REX_SPROCKET_H
#include "../Logger/Logger.h"
#include <iostream>
#include <stdio.h>
#include "errno.h"
#include <string>
#include <cstring>
#include <streambuf>
#include "unistd.h"
#include <sys/wait.h>
#include <pwd.h>
#include <grp.h>
#include <fstream>
#include "fcntl.h"
#include "../loaders/misc/helpers.h"
// exit codes for Rex
enum SPROC_RETURN_CODES {
SUCCESS = true,
UID_NOT_FOUND = -404,
GID_NOT_FOUND = -405,
SET_GID_FAILED = -401,
SET_UID_FAILED = -404,
EXEC_FAILURE_GENERAL = -666,
PIPE_FAILED3 = -996,
PIPE_FAILED2 = -997,
PIPE_FAILED = -998,
DUP2_FAILED = -999,
FORK_FAILED = -1000
};
// executes a subprocess and captures STDOUT, STDERR, and return code.
// should be able to receive path of binary to be executed as well as any parameters
class Sproc {
public:
// call the object. return value is enum representing external execution attempt not binary exit code
static int execute(
std::string shell,
std::string environment_file,
std::string user_name,
std::string group_name,
std::string command,
int LOG_LEVEL,
std::string task_name,
bool log_to_file,
std::string logs_dir
);
};
// a teebuff implementation
class teebuf: public std::streambuf
{
public:
// Construct a streambuf which tees output to both input
// streambufs.
teebuf(std::streambuf * sb1, std::streambuf * sb2);
private:
virtual int overflow(int c);
virtual int sync();
std::streambuf * sb1;
std::streambuf * sb2;
};
// a simple helper class to create a tee stream from two input streams
class teestream : public std::ostream
{
public:
// Construct an ostream which tees output to the supplied ostreams.
teestream( std::ostream & o1, std::ostream & o2);
teebuf tbuf;
};
#endif //REX_SPROCKET_H

346
src/config/Config.cpp Normal file
View File

@ -0,0 +1,346 @@
#include "Config.h"
/**
* @brief General exception class for the Conf class.
*
* This exception class can be thrown to indicate an error in the Conf class. It provides an error message describing the problem.
*/
class ConfigLoadException: public std::exception
{
public:
/**
* @brief Constructor that takes a C-style string error message.
*
* The string contents are copied upon construction. The caller is responsible for deleting the char*.
*
* @param message C-style string error message.
*/
explicit ConfigLoadException(const char* message):
msg_(message)
{}
/**
* @brief Constructor that takes a C++ STL string error message.
*
* @param message The error message.
*/
explicit ConfigLoadException(const std::string& message):
msg_(message)
{}
/**
* @brief Virtual destructor to allow for subclassing.
*/
virtual ~ConfigLoadException() throw (){}
/**
* @brief Returns a pointer to the error description.
*
* @return A pointer to a const char*. The underlying memory is in posession of the Exception object. Callers must not attempt to free the memory.
*/
virtual const char* what() const throw (){
return msg_.c_str();
}
protected:
/**
* @brief Error message.
*/
std::string msg_;
};
/**
* @brief Set the string value of a key
*
* This method sets the value of a key as a string in a member variable.
* It first retrieves the string value of the key using the `get_string` method.
* If the key is not found, a `ConfigLoadException` is thrown.
* If the key is found, the string value is interpolated using the `interpolate` method and then assigned to the member variable.
* The method logs the task in the log file using the `slog.log_task` method.
*
* @param keyname The name of the key to retrieve
* @param object_member The reference to the member variable to store the value
* @param filename The name of the configuration file
*
* @throws ConfigLoadException If the key is not found in the configuration file
*/
void Conf::set_object_s(std::string keyname, std::string & object_member, std::string filename )
{
std::string jval_s;
if ( this->get_string(jval_s, keyname) != 0) {
throw ConfigLoadException( "'" + keyname + "' string is not set in the config file supplied: " + filename );
} else {
interpolate(jval_s);
object_member = jval_s;
}
this->slog.log_task( E_DEBUG, "SET_PROPERTY", "'" + keyname + "': " + object_member );
}
/**
* @brief Set the string value of a key with a derived path
*
* This method sets the value of a key as a string in a member variable with a derived path.
* It first retrieves the string value of the key using the `get_string` method.
* If the key is not found, a `ConfigLoadException` is thrown.
* If the key is found, the string value is interpolated using the `interpolate` method and then passed to the `prepend_project_root` method to derive the path.
* The derived path is then assigned to the member variable.
* The method logs the task in the log file using the `slog.log_task` method.
*
* @param keyname The name of the key to retrieve
* @param object_member The reference to the member variable to store the value
* @param filename The name of the configuration file
*
* @throws ConfigLoadException If the key is not found in the configuration file
*/
void Conf::set_object_s_derivedpath(std::string keyname, std::string & object_member, std::string filename )
{
std::string jval_s;
if ( this->get_string(jval_s, keyname) != 0) {
throw ConfigLoadException( "'" + keyname + "' string is not set in the config file supplied: " + filename );
} else {
interpolate(jval_s);
object_member = prepend_project_root( jval_s );
}
this->slog.log_task( E_DEBUG, "SET_PROPERTY", "'" + keyname + "': " + object_member );
}
/**
* @brief Set the boolean value of a key
*
* This method sets the value of a key as a boolean in a member variable.
* It first retrieves the boolean value of the key using the `get_bool` method.
* If the key is not found, a `ConfigLoadException` is thrown.
* If the key is found, the boolean value is assigned to the member variable.
* The method logs the task in the log file using the `slog.log_task` method.
*
* @param keyname The name of the key to retrieve
* @param object_member The reference to the member variable to store the value
* @param filename The name of the configuration file
*
* @throws ConfigLoadException If the key is not found in the configuration file
*/
void Conf::set_object_b(std::string keyname, bool & object_member, std::string filename )
{
bool jval_b;
if ( this->get_bool(jval_b, keyname) != 0) {
throw ConfigLoadException( "'" + keyname + "' boolean is not set in the config file supplied: " + filename );
} else {
object_member = jval_b;
}
this->slog.log_task( E_DEBUG, "SET_PROPERTY", "'" + keyname + "' " + std::to_string(object_member));
}
/**
* @brief Prepend the project root to a relative path
*
* This method prepends the project root to a given relative path.
* The project root is stored as a member variable in the `Conf` object.
* The method concatenates the project root and the relative path separated by a forward slash (/) and returns the result.
*
* @param relative_path The relative path to prepend the project root to
*
* @return The concatenated path with the project root and the relative path
*/
std::string Conf::prepend_project_root( std::string relative_path)
{
return this->project_root + "/" + relative_path;
}
/**
* @brief Get the absolute path from a relative path
*
* This function takes a relative path and returns the corresponding absolute path.
* The absolute path is obtained by calling the `realpath` function.
* If the `realpath` function returns a null pointer, an error message is printed to the standard error stream and an empty string is returned.
*
* @param relative_path The relative path to be converted to an absolute path
*
* @return The absolute path corresponding to the relative path
*/
std::string get_absolute_path(const std::string &relative_path)
{
char resolved_path[1024];
memset(resolved_path, 0, sizeof(resolved_path));
if( realpath( relative_path.c_str(), resolved_path) == nullptr ) {
std::cerr << "Error resolving path: " << relative_path << std::endl;
return "";
}
return std::string(resolved_path);
}
/**
* @brief Check if a path exists
*
* This method checks if a given path exists.
* If the path exists, a log entry is made in the log file using the `slog.log_task` method with log level `E_DEBUG`.
* If the path does not exist, a log entry is made in the log file with log level `E_FATAL` and a `ConfigLoadException` is thrown.
*
* @param keyname The name of the key to be used in the log entry
* @param path The path to be checked
*
* @throws ConfigLoadException If the path does not exist
*/
void Conf::checkPathExists( std::string keyname, const std::string &path ) {
if ( exists( path ) ) {
this->slog.log_task( E_DEBUG, "SANITY_CHECKS", "'" + keyname + "' exists ('" + path + "')" );
} else {
this->slog.log_task( E_FATAL, "SANITY_CHECKS", "'" + keyname + "' does not exist ('" + path + "')" );
throw ConfigLoadException("Path does not exist.");
}
}
/**
* @brief Load the shells
*
* This method loads the shell definitions from a file.
* The path to the shell definition file is stored as a member variable in the `Conf` object.
* The method first logs the task of loading the shells using the `slog.log_task` method.
* The method then tries to load the JSON file using the `load_json_file` method.
* If an exception occurs while loading the file, a log entry with log level `E_FATAL` is made and a `ConfigLoadException` is thrown.
* The method then retrieves the serialized value of the "shells" key using the `get_serialized` method.
* If the "shells" key is not found, a log entry with log level `E_FATAL` is made and a `ConfigLoadException` is thrown.
* The method then loops through the serialized values, loads each shell definition using the `load_root` method of the `Shell` class, and stores each shell in the `shells` vector.
* The method logs each loaded shell using the `slog.log_task` method.
*
* @throws ConfigLoadException If there is an error parsing the shell definition file
*/
void Conf::load_shells() {
this->slog.log_task( E_DEBUG, "SHELLS", "Loading shells..." );
try {
// load the test file.
this->load_json_file( this->shell_definitions_path );
} catch (std::exception& e) {
this->slog.log_task( E_FATAL, "SHELLS", "Unable to load shell definition file: '" + this->shell_definitions_path + "'. Error: " + e.what());
throw ConfigLoadException("Parsing error in shell definitions file.");
}
Json::Value jbuff;
if ( this-> get_serialized( jbuff, "shells" ) != 0 ) {
this->slog.log_task( E_FATAL, "SHELLS", "Parsing error: '" + this->shell_definitions_path + "'. Error: 'shells' key not found." );
throw ConfigLoadException("Parsing error in shell definitions file.");
}
Shell tmp_S = Shell( this->LOG_LEVEL );
for ( int index = 0; index < jbuff.size(); index++ )
{
tmp_S.load_root( jbuff[index] );
this->shells.push_back( tmp_S );
this->slog.log_task( E_DEBUG, "SHELLS", "Loaded shell: '" + tmp_S.name + "'" );
}
}
/**
* @brief Get a shell by name
*
* This method retrieves a `Shell` object by its name from the `shells` vector.
* The method loops through the `shells` vector and compares each shell's name to the input name.
* If a match is found, the matching shell is returned.
* If no match is found, a `ConfigLoadException` is thrown.
*
* @param name The name of the shell to retrieve
*
* @return The `Shell` object corresponding to the input name
*
* @throws ConfigLoadException If the input name does not match any shell in the `shells` vector
*/
Shell Conf::get_shell_by_name( std::string name ) {
for (auto &shell: this->shells) {
if (shell.name == name) {
return shell;
}
}
throw ConfigLoadException("The shell specified ('" + name + "') is not defined in the shell definitions file.");
}
/**
* @class Conf
* @brief Loads the configuration for the application
*
* This class is responsible for loading the configuration for the application. It uses the `JSON_Loader` class to
* parse the JSON configuration file and stores the relevant information.
*
* @note Currently, the class only supports loading a single configuration file. However, it can be expanded to
* support loading multiple files or detecting when a directory path is supplied for the units_path or plan_path and
* importing all Tasks and Units.
*
* @param filename - The filename to load the configuration from.
* @param LOG_LEVEL - The log level to use for logging messages.
*/
Conf::Conf(std::string filename, int LOG_LEVEL ): JSON_Loader(LOG_LEVEL ), slog(LOG_LEVEL, "_conf_" )
{
this->LOG_LEVEL = LOG_LEVEL;
this->slog.log_task( E_DEBUG, "LOAD", "Loading configuration file: " + filename );
interpolate( filename );
try {
// load the test file.
this->load_json_file( filename );
} catch (std::exception& e) {
this->slog.log( E_FATAL, "Unable to load configuration file: '" + filename + "'. Error: " + e.what());
throw ConfigLoadException("Parsing error in configuration file.");
}
Json::Value jbuff;
if ( this->get_serialized( jbuff, "config" ) != 0) {
this->slog.log_task( E_FATAL, "LOAD", "Unable to locate 'config' object in configuration file: " + filename );
throw ConfigLoadException("Unable to locate 'config' object in configuration file.");
} else {
this->slog.log_task( E_DEBUG, "LOAD", "Found 'config' object in configuration file: " + filename );
this->json_root = jbuff;
}
set_object_s( "project_root", this->project_root, filename );
// convert to an absolute path after all the interpolation is done.
this->project_root = get_absolute_path( this->project_root );
// all other paths are relative to project_root
set_object_s_derivedpath( "units_path", this->units_path, filename );
set_object_s_derivedpath( "logs_path", this->logs_path, filename );
set_object_s_derivedpath( "shells_path", this->shell_definitions_path, filename );
// ensure these paths exists, with exception to the logs_path, which will be created at runtime
this->slog.log_task( E_DEBUG, "SANITY_CHECKS", "Checking for sanity..." );
checkPathExists( "project_root", this->project_root );
checkPathExists( "units_path", this->units_path );
checkPathExists( "shells_path", this->shell_definitions_path );
// shells are scoped beyond plan so they need to be considered part of config
load_shells();
this->slog.log_task( E_DEBUG, "LOAD", "CONFIGURATION LOADED." );
}
/**
* @brief Gets the path to the Unit definition file
*
* This function returns the path to the Unit definition file that was specified in the configuration file.
*
* @return The path to the Unit definition file.
*/
std::string Conf::get_units_path() { return this->units_path; }
/**
* @brief Gets the path to the logs directory
*
* This function returns the path to the logs directory that was specified in the configuration file.
*
* @return The path to the logs directory.
*/
std::string Conf::get_logs_path() { return this->logs_path; }
/**
* @brief Gets the project root directory
*
* This function returns the path to the project root directory that was specified in the configuration file.
*
* @return The path to the project root directory.
*/
std::string Conf::get_project_root() { return this->project_root; }

171
src/config/Config.h Normal file
View File

@ -0,0 +1,171 @@
/*
Rex - A configuration management and workflow automation tool that
compiles and runs in minimal environments.
© SILO GROUP LLC, 2023.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef REX_CONF_H
#define REX_CONF_H
#include <exception>
#include <string>
#include "../json_support/JSON.h"
#include "../logger/Logger.h"
#include "../misc/helpers.h"
#include "../shells/shells.h"
#define STRINGIZE2(s) #s
#define STRINGIZE(s) STRINGIZE2(s)
#define IMPL_CONFIG_VERSION 6
#define VERSION_STRING STRINGIZE(IMPL_CONFIG_VERSION)
/**
* @class Conf
* @brief This class loads configuration from a JSON file and provides access to the configuration values.
*
* The class `Conf` extends the `JSON_Loader` class and implements a constructor that takes the filename of the configuration file and the LOG_LEVEL to be used by the internal `Logger` instance.
* The class provides getter methods to access the `units_path`, `logs_path`, and `project_root` values.
*/
class Conf: public JSON_Loader {
public:
/**
* @brief Constructs a Conf object and loads the configuration from the specified file
*
* @param filename The name of the configuration file
* @param LOG_LEVEL The log level to be used by the internal Logger instance
*/
Conf(std::string filename, int LOG_LEVEL);
/**
* @brief Returns the path to the units directory
*
* @return The path to the units directory
*/
std::string get_units_path();
/**
* @brief Returns the path to the logs directory
*
* @return The path to the logs directory
*/
std::string get_logs_path();
/**
* @brief Returns the root directory of the project
*
* @return The root directory of the project
*/
std::string get_project_root();
/**
* @brief Returns the Shell object with the specified name
*
* @param name The name of the Shell object
*
* @return The Shell object with the specified name
*/
Shell get_shell_by_name(std::string name);
private:
/**
* @brief The path to the units directory
*/
std::string units_path;
/**
* @brief The path to the logs directory
*/
std::string logs_path;
/**
* @brief The root directory of the project
*/
std::string project_root;
/**
* @brief The path to the shell definitions
*/
std::string shell_definitions_path;
/**
* @brief The vector of Shell objects
*/
std::vector<Shell> shells;
/**
* @brief Checks if the specified path exists
*
* @param keyname The name of the key in the JSON file
* @param path The path to be checked
*/
void checkPathExists(std::string keyname, const std::string &path);
/**
* @brief The log level to be used by the internal Logger instance
*/
int LOG_LEVEL;
/**
* @brief The internal Logger instance
*/
Logger slog;
/**
* @brief Prepends the project root to a relative path
*
* @param relative_path The relative path to be modified
*
* @return The modified path with the project root prepended
*/
std::string prepend_project_root(std::string relative_path);
/**
* @brief Sets a string object member from a JSON file
*
* @param keyname The name of the key in the JSON file
* @param object_member The string object member to be set
* @param filename The name of the JSON file
*/
void set_object_s(std::string keyname, std::string &object_member, std::string filename);
/**
* @brief Sets a string object member from a JSON file, with the path derived from the project root
*
* @param keyname The name of the key in the JSON file
* @param object_member The string object member to be set
* @param filename The name of the JSON file
*/
void set_object_s_derivedpath(std::string keyname, std::string &object_member, std::string filename);
/**
* @brief Sets a boolean object member from a JSON file
*
* @param keyname The name of the key in the JSON file
* @param object_member The boolean object member to be set
* @param filename The name of the JSON file
*/
void set_object_b(std::string keyname, bool &object_member, std::string filename);
/**
* @brief Loads the shell definitions from the specified file
*/
void load_shells();
};
#endif //REX_CONF_H

270
src/json_support/JSON.cpp Normal file
View File

@ -0,0 +1,270 @@
/*
Rex - A configuration management and workflow automation tool that
compiles and runs in minimal environments.
© SILO GROUP LLC, 2023.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "JSON.h"
/**
* @class JSON_Loader_NotReady
* @brief Exception thrown when a member function is called before data is populated.
*
* This class is derived from the standard library class `std::runtime_error` and is thrown
* when a member function is called before the JSON data has been populated.
*/
class JSON_Loader_NotReady : public std::runtime_error
{
public:
/**
* @brief Constructor for the JSON_Loader_NotReady exception.
*
* This constructor creates an instance of the JSON_Loader_NotReady exception and sets its error message.
*/
JSON_Loader_NotReady() : std::runtime_error("JSON_Loader: Tried to access JSON without actually populating JSON.") {}
};
/**
* @class JSON_Loader_FileNotFound
* @brief Exception thrown when the requested file could not be found.
*
* This class is derived from the standard library class `std::runtime_error` and is thrown
* when the file specified in the JSON_Loader constructor could not be found.
*/
class JSON_Loader_FileNotFound : public std::runtime_error
{
public:
/**
* @brief Constructor for the JSON_Loader_FileNotFound exception.
*
* This constructor creates an instance of the JSON_Loader_FileNotFound exception and sets its error message.
*/
JSON_Loader_FileNotFound() : std::runtime_error("JSON_Loader: The requested file could not be found.") {}
};
/**
* @class JSON_Loader_InvalidJSON
* @brief Exception thrown when the provided JSON could not be parsed.
*
* This class is derived from the standard library class `std::runtime_error` and is thrown
* when the JSON data provided to the JSON_Loader could not be parsed.
*/
class JSON_Loader_InvalidJSON : public std::runtime_error
{
public:
/**
* @brief Constructor for the JSON_Loader_InvalidJSON exception.
*
* This constructor creates an instance of the JSON_Loader_InvalidJSON exception and sets its error message.
*/
JSON_Loader_InvalidJSON() : std::runtime_error("JSON_Loader: The JSON provided could not be parsed.") {}
};
/**
* @class JSON_Loader
* @brief The base class for loading and parsing JSON data.
*
* The JSON_Loader class provides the functionalities shared between its derived classes, Suite and Plan.
* It is responsible for initializing the JSON data to an unpopulated state.
*/
/**
* @brief Constructor for the JSON_Loader base class.
*
* This constructor initializes the JSON data to an unpopulated state and sets the logging level.
*
* @param LOG_LEVEL The logging level for the JSON_Loader instance.
*/
JSON_Loader::JSON_Loader(int LOG_LEVEL) : slog(LOG_LEVEL, "_json_")
{
this->populated = false;
this->LOG_LEVEL = LOG_LEVEL;
}
/**
* @brief Loads a JSON-formatted string into the JSON_Loader instance.
*
* This function takes a JSON-formatted string as input, parses it using a Json::Reader, and stores the result in the protected member `json_root`.
* If the parsing is successful, the `populated` flag is set to `true`.
*
* @param input The JSON-formatted string to be loaded into the JSON_Loader instance.
* @throws JSON_Loader_InvalidJSON if the input string could not be parsed.
*/
void JSON_Loader::load_json_string(std::string input)
{
Json::Reader json_reader;
std::ifstream json_file_ifstream(input.c_str(), std::ifstream::binary);
bool parsingSuccessful = json_reader.parse(json_file_ifstream, this->json_root);
if (!parsingSuccessful)
{
this->slog.log(E_FATAL, "Failed to parse adhoc JSON value: " + json_reader.getFormattedErrorMessages());
throw JSON_Loader_InvalidJSON();
}
else
{
this->slog.log(E_DEBUG, "Successfully parsed JSON string with " + std::to_string(this->json_root.size()) + " elements. Value: '" + input + "'.");
}
this->populated = true;
}
/**
* @brief Loads a JSON-formatted file into the JSON_Loader instance.
*
* This function takes a file path as input, parses the file using a Json::Reader, and stores the result in the protected member `json_root`.
* If the parsing is successful, the `populated` flag is set to `true`.
*
* @param filename The file path to the JSON-formatted file to be loaded into the JSON_Loader instance.
* @throws JSON_Loader_FileNotFound if the file specified in `filename` could not be found.
* @throws JSON_Loader_InvalidJSON if the contents of the file could not be parsed.
*/
void JSON_Loader::load_json_file(std::string filename)
{
Json::Reader json_reader;
if (!exists(filename))
{
this->slog.log_task(E_FATAL, "LOAD", "File '" + filename + "' does not exist.");
throw JSON_Loader_FileNotFound();
}
std::ifstream json_file_ifstream(filename, std::ifstream::binary);
bool parsingSuccessful = json_reader.parse(json_file_ifstream, this->json_root);
if (!parsingSuccessful)
{
this->slog.log_task(E_FATAL, "PARSING", "Failed to parse file '" + filename + "': " + json_reader.getFormattedErrorMessages());
throw JSON_Loader_InvalidJSON();
}
else
{
this->slog.log_task(E_DEBUG, "PARSING", "Parsed '" + filename + "' with " + std::to_string(this->json_root.size()) + " element(s).");
}
this->populated = true;
}
/**
* @brief Returns the string representation of the `json_root` member.
*
* This function returns the string representation of the `json_root` member, which contains the parsed JSON data.
*
* @return The string representation of the `json_root` member.
* @throws JSON_Loader_NotReady if the JSON data has not yet been populated.
*/
std::string JSON_Loader::as_string()
{
if (!this->populated)
{
throw JSON_Loader_NotReady();
}
return this->json_root.asString();
}
/**
* @brief Assigns the serialized representation of a specified key to a passed Json::Value reference.
*
* This function takes a reference to a Json::Value object as input, along with a string representing the key to search for.
* If the key is found in the `json_root` member, its value is assigned to the passed Json::Value reference.
*
* @param input A reference to the Json::Value object to receive the value of the specified key.
* @param key The key name to search for in the `json_root` member.
* @return 0 if the key was found and its value was assigned to the passed Json::Value reference, 1 if the key was not found.
* @throws JSON_Loader_NotReady if the JSON data has not yet been populated.
*/
int JSON_Loader::get_serialized(Json::Value &input, std::string key)
{
if (!this->populated)
{
throw JSON_Loader_NotReady();
}
if (this->json_root.isMember(key))
{
input = this->json_root[key];
return 0;
}
this->slog.log_task(E_FATAL, "PARSING", "Failed to find key '" + key + "'.");
return 1;
}
/**
* @brief Assigns the string representation of a specified key to a passed string reference.
*
* This function takes a reference to a string object as input, along with a string representing the key to search for.
* If the key is found in the `json_root` member, its string representation is assigned to the passed string reference.
*
* @param input A reference to the string object to receive the string representation of the specified key.
* @param key The key name to search for in the `json_root` member.
* @return 0 if the key was found and its string representation was assigned to the passed string reference, 1 if the key was not found.
* @throws JSON_Loader_NotReady if the JSON data has not yet been populated.
*/
int JSON_Loader::get_string(std::string &input, std::string key)
{
if (!this->populated)
{
throw JSON_Loader_NotReady();
}
if (this->json_root.isMember(key))
{
input = this->json_root[key].asString();
return 0;
}
this->slog.log_task(E_FATAL, "PARSING", "Failed to find key '" + key + "'.");
return 1;
}
/**
* @brief Assigns the boolean representation of a specified key to a passed bool reference.
*
* This function takes a reference to a bool object as input, along with a string representing the key to search for.
* If the key is found in the `json_root` member, its boolean representation is assigned to the passed bool reference.
*
* @param input A reference to the bool object to receive the boolean representation of the specified key.
* @param key The key name to search for in the `json_root` member.
* @return 0 if the key was found and its boolean representation was assigned to the passed bool reference, 1 if the key was not found.
* @throws JSON_Loader_NotReady if the JSON data has not yet been populated.
*/
int JSON_Loader::get_bool(bool &input, std::string key)
{
if (!this->populated)
{
throw JSON_Loader_NotReady();
}
if (this->json_root.isMember(key))
{
input = this->json_root[key].asBool();
return 0;
}
this->slog.log_task(E_FATAL, "PARSING", "Failed to find key '" + key + "'.");
return 1;
}

112
src/json_support/JSON.h Normal file
View File

@ -0,0 +1,112 @@
#ifndef REX_JSON_H
#define REX_JSON_H
#include "jsoncpp/json.h"
#include <iostream>
#include <fstream>
#include <cstdlib>
#include <stdexcept>
#include "../misc/helpers.h"
#include "../logger/Logger.h"
/**
* @class JSON_Loader
* @brief Loads and parses JSON data
*
* This class is responsible for loading and parsing JSON data from a file or a string. It uses the `JsonCpp` library to
* handle the parsing and provides functions to access the data in a safe and convenient way.
*
* @param LOG_LEVEL - The log level to use for logging messages.
*/
class JSON_Loader {
protected:
/// The root of the JSON data
Json::Value json_root;
/// Indicates whether the JSON data has been successfully loaded and parsed
bool populated;
public:
/**
* @brief Constructor for the JSON_Loader class
*
* This constructor initializes the JSON_Loader object and sets the log level to use for logging messages.
*
* @param LOG_LEVEL - The log level to use for logging messages.
*/
JSON_Loader(int LOG_LEVEL);
/**
* @brief Loads JSON data from a file
*
* This function loads JSON data from a file and parses it using the `JsonCpp` library.
*
* @param filename - The path to the file to load the JSON data from.
*/
void load_json_file(std::string filename);
/**
* @brief Loads JSON data from a string
*
* This function parses JSON data from a string using the `JsonCpp` library.
*
* @param input - The string containing the JSON data.
*/
void load_json_string(std::string input);
/**
* @brief Returns the JSON data as a string
*
* This function returns the JSON data in string form.
*
* @return The JSON data as a string.
*/
std::string as_string();
/**
* @brief Gets the value of a key in the JSON data
*
* This function retrieves the value of a key in the JSON data and returns it in a `Json::Value` object. If the
* key does not exist, an error is logged and a default `Json::Value` object is returned.
*
* @param input - A reference to a `Json::Value` object to store the result.
* @param key - The key to retrieve the value for.
*
* @return 0 if the key was found and its value was retrieved successfully, 1 otherwise.
*/
int get_serialized(Json::Value &input, std::string key);
/**
* @brief Gets the string value of a key in the JSON data
*
* This function retrieves the string value of a key in the JSON data and returns it in a `std::string` object.
* If the key does not exist or its value is not a string, an error is logged and a default `std::string` object
* is returned.
*
* @param input - A reference to a `std::string` object to store the result.
* @param key - The key to retrieve the string value for.
*
* @return 0 if the key was found and its string value was retrieved successfully, 1 otherwise.
*/
int get_string(std::string &input, std::string key);
/**
* @brief Gets the boolean value of a key in the JSON data
*
* This function retrieves the boolean value of a key in the JSON data and returns it in a `bool` object. If the
* key does not exist or its value is not a boolean, an error is logged and a default `bool` object is returned.
*
* @param input - A reference to a `bool` object to store the result.
* @param key - The key to retrieve the boolean value for.
*
* @return 0 if the key was found and its boolean value was retrieved successfully, 1 otherwise.
*/
int get_bool(bool &input, std::string key);
private:
/// The logger object used for logging messages
Logger slog;
/// The log level to use for logging messages
int LOG_LEVEL;
};
#endif //REX_JSON_H

70
src/lcpex/Contexts.cpp Normal file
View File

@ -0,0 +1,70 @@
#include "Contexts.h"
// converts username to UID
int username_to_uid( std::string username, int & uid )
{
// assume failure unless proven otherwise
int r_code = false;
struct passwd * pw;
if ( ( pw = getpwnam( username.c_str() ) ) != NULL )
{
// successful user lookup
r_code = true;
uid = pw->pw_uid;
} else {
// failed lookup, do nothing, assumed failure
}
return r_code;
};
// converts group name to GID
int groupname_to_gid( std::string groupname, int & gid )
{
int r_code = false;
struct group * gp;
if ( ( gp = getgrnam( groupname.c_str() ) ) != NULL )
{
r_code = true;
gid = gp->gr_gid;
} else {
// failed lookup, do nothing, assumed failure
}
return r_code;
}
// SET PROCESS TO A CERTAIN IDENTITY CONTEXT
int set_identity_context( std::string user_name, std::string group_name ) {
// the UID and GID for the username and groupname provided for context setting
int context_user_id;
int context_group_id;
int res = 0;
// convert username to UID
if (! ( res = username_to_uid(user_name, context_user_id ) ) )
{
return ERROR_NO_SUCH_USER;
}
// convert group name to GID
if (! ( res = groupname_to_gid(group_name, context_group_id ) ) )
{
return ERROR_NO_SUCH_GROUP;
}
if ( ( res = setgid(context_group_id) ) ) {
perror("lcpex: setgid failed");
return ERROR_SETGID_FAILED;
}
if ( ( res = setuid(context_user_id) ) ) {
perror("lcpex: setuid failed");
return ERROR_SETUID_FAILED;
}
return ERROR_NONE;
}

69
src/lcpex/Contexts.h Normal file
View File

@ -0,0 +1,69 @@
#ifndef LCPEX_CONTEXTS_H
#define LCPEX_CONTEXTS_H
#include <string>
#include <csignal>
#include <pwd.h>
#include <grp.h>
#include <iostream>
enum IDENTITY_CONTEXT_ERRORS {
ERROR_NONE = 0,
ERROR_NO_SUCH_USER,
ERROR_NO_SUCH_GROUP,
ERROR_NO_SUCH_USER_IN_GROUP,
ERROR_SETGID_FAILED,
ERROR_SETUID_FAILED,
};
/**
* @brief Converts a username to a user ID (UID)
*
* @param username The username to convert
* @param uid A reference to the resulting UID
*
* @return A boolean indicating whether the conversion was successful
*
* This function takes a username as a string and returns the corresponding UID in the `uid` parameter.
* The function returns a boolean indicating whether the conversion was successful.
* If the username is not found, the function returns false and the value of `uid` is unchanged.
*/
int username_to_uid( std::string username, int & uid );
/**
* @brief Converts a group name to a group ID (GID)
*
* @param groupname The group name to convert
* @param gid A reference to the resulting GID
*
* @return A boolean indicating whether the conversion was successful
*
* This function takes a group name as a string and returns the corresponding GID in the `gid` parameter.
* The function returns a boolean indicating whether the conversion was successful.
* If the group name is not found, the function returns false and the value of `gid` is unchanged.
*/
int groupname_to_gid( std::string groupname, int & gid );
/**
* @brief Sets the execution context for a process
*
* @param user_name The username to use for the execution context
* @param group_name The group name to use for the execution context
*
* @return An error code indicating the result of the context setting
*
* This function takes a username and group name as input and sets the execution context for the process.
* The function converts the username and group name to UID and GID respectively, and sets the process's
* user and group identity using the setuid() and setgid() functions.
*
* If either the username or group name is not found, the function returns `ERROR_NO_SUCH_USER` or `ERROR_NO_SUCH_GROUP` respectively.
* If the call to setgid() fails, the function returns `ERROR_SETGID_FAILED`.
* If the call to setuid() fails, the function returns `ERROR_SETUID_FAILED`.
* If the context setting is successful, the function returns `ERROR_NONE`.
*/
int set_identity_context( std::string user_name, std::string group_name );
#endif //LCPEX_CONTEXTS_H

21
src/lcpex/helpers.h Normal file
View File

@ -0,0 +1,21 @@
#ifndef LCPEX_HELPERS_H
#define LCPEX_HELPERS_H
// helper for sanity
enum PIPE_ENDS {
READ_END = 0,
WRITE_END = 1
};
// helper for sanity
// these do NOT represent fd values
enum CHILD_PIPE_NAMES {
STDOUT_READ = 0,
STDERR_READ = 1
};
#define BUFFER_SIZE 1024
#endif //LCPEX_HELPERS_H

338
src/lcpex/liblcpex.cpp Normal file
View File

@ -0,0 +1,338 @@
#include "liblcpex.h"
std::string prefix_generator(
std::string command,
bool is_shell_command,
std::string shell_path,
std::string shell_execution_arg,
bool supply_environment,
std::string shell_source_subcommand,
std::string environment_file_path
) {
std::string prefix = "";
if ( is_shell_command ) {
// it's a shell command, so we need to set up a shell execution of the command
prefix = shell_path;
// if the shell takes an argument to execute a command, add it enclosed in quotes
if (shell_execution_arg != "") {
// add the execution arg
prefix += " " + shell_execution_arg + " ";
} else {
// no execution arg, so add a space
prefix += " ";
}
if (supply_environment) {
// add the source subcommand
prefix += "'" + shell_source_subcommand + " " + environment_file_path + " && " + command + "'";
} else {
// no environment supplied, so just add the command
prefix += "'" + command + "'";
}
} else {
// it's not a shell command, so we can just execute it directly
prefix = command;
}
std::cout << "prefix: " << prefix << std::endl;
return prefix;
}
int lcpex(
std::string command,
std::string stdout_log_file,
std::string stderr_log_file,
bool context_override,
std::string context_user,
std::string context_group,
bool force_pty,
bool is_shell_command,
std::string shell_path,
std::string shell_execution_arg,
bool supply_environment,
std::string shell_source_subcommand,
std::string environment_file_path
) {
// generate the prefix
std::string prefix = prefix_generator(
command,
is_shell_command,
shell_path,
shell_execution_arg,
supply_environment,
shell_source_subcommand,
environment_file_path
);
command = prefix;
// if we are forcing a pty, then we will use the vpty library
if( force_pty )
{
return exec_pty( command, stdout_log_file, stderr_log_file, context_override, context_user, context_group, supply_environment );
}
// otherwise, we will use the execute function
return execute( command, stdout_log_file, stderr_log_file, context_override, context_user, context_group, supply_environment );
}
/**
* @brief Set the close-on-exec flag of a file descriptor
*
* This function uses the `fcntl` function to set the close-on-exec flag
* of the specified file descriptor `fd`. If the `fcntl` call fails,
* the function will print an error message to `stderr` and exit with status 1.
*
* @param fd The file descriptor for which to set the close-on-exec flag
*/
void set_cloexec_flag(int fd)
{
if (fcntl(fd, F_SETFD, FD_CLOEXEC) == -1)
{
perror("fcntl");
exit(1);
}
}
/**
* @brief This function is executed by the child process to run a shell command
*
* @param context_override Indicates whether to override the current execution context
* @param context_user The user to switch to for execution context
* @param context_group The group to switch to for execution context
* @param processed_command The command to be executed, after processing
* @param fd_child_stdout_pipe The file descriptor for the child process's standard output pipe
* @param fd_child_stderr_pipe The file descriptor for the child process's standard error pipe
*
* The run_child_process() function takes the parameters context_override, context_user, context_group, processed_command,
* fd_child_stdout_pipe and fd_child_stderr_pipe.
*
* If context_override is set to true, the child process will run under the specified context_user and context_group.
* If either the context_user or context_group does not exist, an error message will be displayed and the process will exit.
*
* The function first redirects the child process's standard output and standard error to pipes.
*
* If context_override is set to true, the child process sets its identity context using the set_identity_context() function.
* If an error occurs while setting the identity context, a message will be displayed and the process will exit.
*
* Finally, the child process calls execvp() with the processed_command to run the shell command.
* If the execvp() function fails, an error message is displayed.
*/
void run_child_process(bool context_override, const char* context_user, const char* context_group, char* processed_command[], int fd_child_stdout_pipe[], int fd_child_stderr_pipe[]) {
while ((dup2(fd_child_stdout_pipe[WRITE_END], STDOUT_FILENO) == -1) && (errno == EINTR)) {}
while ((dup2(fd_child_stderr_pipe[WRITE_END], STDERR_FILENO) == -1) && (errno == EINTR)) {}
if ( context_override ) {
int context_result = set_identity_context(context_user, context_group);
switch (context_result) {
case IDENTITY_CONTEXT_ERRORS::ERROR_NONE:
break;
case IDENTITY_CONTEXT_ERRORS::ERROR_NO_SUCH_USER:
std::cerr << "lcpex: Aborting: context user not found: " << context_user << std::endl;
exit(1);
break;
case IDENTITY_CONTEXT_ERRORS::ERROR_NO_SUCH_GROUP:
std::cerr << "lcpex: Aborting: context group not found: " << context_group << std::endl;
exit(1);
break;
case IDENTITY_CONTEXT_ERRORS::ERROR_SETGID_FAILED:
std::cerr << "lcpex: Aborting: Setting GID failed: " << context_user << "/" << context_group << std::endl;
exit(1);
break;
case IDENTITY_CONTEXT_ERRORS::ERROR_SETUID_FAILED:
std::cerr << "lcpex: Aborting: Setting UID failed: " << context_user << "/" << context_group << std::endl;
exit(1);
break;
default:
std::cerr << "lcpex: Aborting: Unknown error while setting identity context." << std::endl;
exit(1);
break;
}
}
int exit_code = execvp(processed_command[0], processed_command);
perror("failed on execvp in child");
exit(exit_code);
}
int execute(
std::string command,
std::string stdout_log_file,
std::string stderr_log_file,
bool context_override,
std::string context_user,
std::string context_group,
bool environment_supplied
){
// this does three things:
// - execute a dang string as a subprocess command
// - capture child stdout/stderr to respective log files
// - TEE child stdout/stderr to parent stdout/stderr
// turn our command string into something execvp can consume
char ** processed_command = expand_env(command );
// if the user chose to supply the environment, then we need to clear the environment
// before we fork the process, so that the child process will inherit the environment
// from the parent process
if ( environment_supplied ) {
clearenv();
}
// open file handles to the two log files we need to create for each execution
FILE * stdout_log_fh = fopen( stdout_log_file.c_str(), "a+" );
FILE * stderr_log_fh = fopen( stderr_log_file.c_str(), "a+" );
// create the pipes for the child process to write and read from using its stdin/stdout/stderr
int fd_child_stdout_pipe[2];
int fd_child_stderr_pipe[2];
if ( pipe(fd_child_stdout_pipe ) == -1 ) {
perror( "child stdout pipe" );
exit( 1 );
}
if ( pipe( fd_child_stderr_pipe ) == -1 ) {
perror( "child stderr pipe" );
exit( 1 );
}
// using O_CLOEXEC to ensure that the child process closes the file descriptors
set_cloexec_flag( fd_child_stdout_pipe[READ_END] );
set_cloexec_flag( fd_child_stderr_pipe[READ_END] );
set_cloexec_flag( fd_child_stdout_pipe[WRITE_END] );
set_cloexec_flag( fd_child_stderr_pipe[WRITE_END] );
// status result basket for the parent process to capture the child's exit status
int status = 616;
pid_t pid = fork();
switch( pid ) {
case -1:
{
// fork failed
perror("fork failure");
exit(1);
}
case 0:
{
// child process
run_child_process(
context_override,
context_user.c_str(),
context_group.c_str(),
processed_command,
fd_child_stdout_pipe,
fd_child_stderr_pipe
);
}
default:
{
// parent process
// The parent process has no need to access the entrance to the pipe, so fd_child_*_pipe[1|0] should be closed
// within that process too:
close(fd_child_stdout_pipe[WRITE_END]);
close(fd_child_stderr_pipe[WRITE_END]);
// attempt to write to stdout,stderr from child as well as to write each to file
char buf[BUFFER_SIZE];
// contains the byte count of the last read from the pipe
ssize_t byte_count;
// watched_fds for poll() to wait on
struct pollfd watched_fds[2];
// populate the watched_fds array
// child STDOUT to parent
watched_fds[CHILD_PIPE_NAMES::STDOUT_READ].fd = fd_child_stdout_pipe[READ_END];
watched_fds[CHILD_PIPE_NAMES::STDOUT_READ].events = POLLIN;
// child STDERR to parent
watched_fds[CHILD_PIPE_NAMES::STDERR_READ].fd = fd_child_stderr_pipe[READ_END];
watched_fds[CHILD_PIPE_NAMES::STDERR_READ].events = POLLIN;
// number of files poll() reports as ready
int num_files_readable;
// loop flag
bool break_out = false;
// loop until we've read all the data from the child process
while ( ! break_out ) {
num_files_readable = poll(watched_fds, sizeof(watched_fds) / sizeof(watched_fds[0]), -1);
if (num_files_readable == -1) {
// error occurred in poll()
perror("poll");
exit(1);
}
if (num_files_readable == 0) {
// poll reports no files readable
break_out = true;
break;
}
for (int this_fd = 0; this_fd < 2; this_fd++) {
if (watched_fds[this_fd].revents & POLLIN) {
// this pipe is readable
byte_count = read(watched_fds[this_fd].fd, buf, BUFFER_SIZE);
if (byte_count == -1) {
// error reading from pipe
perror("read");
exit(EXIT_FAILURE);
} else if (byte_count == 0) {
// reached EOF on one of the streams but not a HUP
// we've read all we can this cycle, so go to the next fd in the for loop
continue;
} else {
// byte count was sane
// write to stdout,stderr
if (this_fd == CHILD_PIPE_NAMES::STDOUT_READ) {
// the child's stdout pipe is readable
write(stdout_log_fh->_fileno, buf, byte_count);
write(STDOUT_FILENO, buf, byte_count);
} else if (this_fd == CHILD_PIPE_NAMES::STDERR_READ) {
// the child's stderr pipe is readable
write(stderr_log_fh->_fileno, buf, byte_count);
write(STDERR_FILENO, buf, byte_count);
} else {
// this should never happen
perror("Logic error!");
exit(EXIT_FAILURE);
}
}
}
if (watched_fds[this_fd].revents & POLLERR) {
close(watched_fds[this_fd].fd);
break_out = true;
}
if (watched_fds[this_fd].revents & POLLHUP) {
// this pipe has hung up
close(watched_fds[this_fd].fd);
break_out = true;
}
}
}
// wait for child to exit, capture status
waitpid(pid, &status, 0);
// close the log file handles
fclose(stdout_log_fh);
fclose(stderr_log_fh);
if WIFEXITED(status) {
return WEXITSTATUS(status);
} else {
return -617;
}
}
}
}

122
src/lcpex/liblcpex.h Normal file
View File

@ -0,0 +1,122 @@
#ifndef LCPEX_LIBLCPEX_H
#define LCPEX_LIBLCPEX_H
#include <cstdio>
#include <cstdlib>
#include <poll.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/wait.h>
#include <iostream>
#include "string_expansion/string_expansion.h"
#include "helpers.h"
#include "Contexts.h"
#include "vpty/pty_fork_mod/tty_functions.h"
#include "vpty/pty_fork_mod/pty_fork.h"
#include "vpty/libclpex_tty.h"
/**
* @brief This function is executed by the child process to run a shell command
*
* @param context_override Indicates whether to override the current execution context
* @param context_user The user to switch to for execution context
* @param context_group The group to switch to for execution context
* @param processed_command The command to be executed, after processing
* @param fd_child_stdout_pipe The file descriptor for the child process's standard output pipe
* @param fd_child_stderr_pipe The file descriptor for the child process's standard error pipe
*
* If context_override is set to true, the child process will run under the specified context_user and context_group.
* If either the context_user or context_group does not exist, an error message will be displayed and the process will exit.
*
* The function first redirects the child process's standard output and standard error to pipes.
*
* If context_override is set to true, the child process sets its identity context using the set_identity_context() function.
* If an error occurs while setting the identity context, a message will be displayed and the process will exit.
*
* Finally, the child process calls execvp() with the processed_command to run the shell command.
* If the execvp() function fails, an error message is displayed.
*/
int execute(
std::string command,
std::string stdout_log_file,
std::string stderr_log_file,
bool context_override,
std::string context_user,
std::string context_group,
bool environment_supplied
);
/**
* @brief Executes a command with logging and optional context switching
*
* @param command The command to be executed
* @param stdout_log_file The file to log the standard output of the command
* @param stderr_log_file The file to log the standard error of the command
* @param context_override Indicates whether to override the current execution context
* @param context_user The user to switch to for execution context
* @param context_group The group to switch to for execution context
* @param force_pty Indicates whether to force a pseudoterminal (pty) for the command execution
* @param is_shell_command Indicates whether the command is a shell command
* @param shell_path The path to the shell executable
* @param shell_execution_arg The argument used to execute a command in the shell
* @param supply_environment Indicates whether to supply an environment
* @param shell_source_subcommand The shell subcommand used to source the environment file
* @param environment_file_path The path to the environment file
*
* @return The exit status of the executed command
*
* This function executes a command with logging and optional context switching. The function generates
* a prefix for the command using the `prefix_generator` function, which sets up a shell execution if
* the command is a shell command. If a pseudoterminal (pty) is forced, the `exec_pty` function is used
* to execute the command, otherwise the `execute` function is used. The standard output and standard
* error of the command are logged to the provided log files. If context overriding is requested, the
* execution context is switched to the provided user and group.
*/
int lcpex(
std::string command,
std::string stdout_log_file,
std::string stderr_log_file,
bool context_override,
std::string context_user,
std::string context_group,
bool force_pty,
bool is_shell_command,
std::string shell_path,
std::string shell_execution_arg,
bool supply_environment,
std::string shell_source_subcommand,
std::string environment_file_path
);
/**
* @brief Generates a command prefix for execution
*
* @param command The main command to be executed
* @param is_shell_command Indicates whether the command is a shell command
* @param shell_path The path to the shell executable
* @param shell_execution_arg The argument used to execute a command in the shell
* @param supply_environment Indicates whether to supply an environment
* @param shell_source_subcommand The shell subcommand used to source the environment file
* @param environment_file_path The path to the environment file
*
* @return The generated command prefix
*
* This function generates a prefix for a command to be executed, based on the provided parameters.
* If the command is a shell command, the prefix will be set up for a shell execution. If the shell
* takes an argument to execute a command, it will be added to the prefix. If an environment is to be
* supplied, the shell subcommand for sourcing the environment file and the environment file path will
* be added to the prefix. If the command is not a shell command, the command will be returned as is.
*/
std::string prefix_generator(
std::string command,
bool is_shell_command,
std::string shell_path,
std::string shell_execution_arg,
bool supply_environment,
std::string shell_source_subcommand,
std::string environment_file_path
);
#endif //LCPEX_LIBLCPEX_H

View File

@ -0,0 +1,24 @@
#include "string_expansion.h"
// convert a string to a char** representing our artificial argv to be consumed by execvp
char ** expand_env(const std::string& var, int flags )
{
std::vector<std::string> vars;
wordexp_t p;
if(!wordexp(var.c_str(), &p, flags))
{
if(p.we_wordc)
for(char** exp = p.we_wordv; *exp; ++exp)
vars.push_back(exp[0]);
wordfree(&p);
}
char ** arr = new char*[vars.size() + 1];
for(size_t i = 0; i < vars.size(); i++){
arr[i] = new char[vars[i].size() + 1];
strcpy(arr[i], vars[i].c_str());
}
arr[vars.size()] = (char * ) nullptr;
return arr;
}

View File

@ -0,0 +1,29 @@
#ifndef LCPEX_STRING_EXPANSION_H
#define LCPEX_STRING_EXPANSION_H
#include <wordexp.h>
#include <vector>
#include <cstring>
#include <iostream>
/**
* @brief Convert a string to a 'char**' representing an argument list
*
* This function takes a string 'var' and converts it into a 'char**'
* representing an argument list. The argument list can be consumed by
* functions like 'execvp'.
*
* @param[in] var The string to be converted
* @param[in] flags Flags controlling the behavior of the conversion
*
* @return A 'char**' representing the argument list
*
* The function uses the 'wordexp' function to perform the conversion. The
* 'flags' argument controls the behavior of the 'wordexp' function. The
* returned 'char**' is dynamically allocated, and must be freed by the
* caller using 'delete[]'.
*/
char ** expand_env(const std::string& var, int flags = 0);
#endif //LCPEX_STRING_EXPANSION_H

View File

@ -0,0 +1,282 @@
#include "libclpex_tty.h"
/**
* @brief Safely prints a message and exits the program
*
* @param msg The message to print
* @param ttyOrig A pointer to a struct termios representing the original terminal settings
*
* This function takes a message `msg` and a pointer to a struct termios `ttyOrig` as input.
* The function first prints the `msg` to `stderr`.
* Then, it calls `ttyResetExit()` with `ttyOrig` as its argument to reset the terminal settings.
* Finally, the function exits the program with exit code 1.
*/
void safe_perror( const char * msg, struct termios * ttyOrig )
{
std::cerr << msg << std::endl;
ttyResetExit( ttyOrig );
exit(1);
}
/**
* @brief Executes the child process
*
* @param fd_child_stderr_pipe A file descriptor array for the child process's stderr pipe
* @param processed_command An array of char pointers representing the command and its arguments to be executed
* @param ttyOrig A pointer to a struct termios representing the original terminal settings
* @param context_override A flag indicating whether to override the process's execution context
* @param context_user The username to use for the execution context if `context_override` is `true`
* @param context_group The group name to use for the execution context if `context_override` is `true`
*
* This function takes an array of file descriptors `fd_child_stderr_pipe` for the child process's stderr pipe,
* an array of char pointers `processed_command` representing the command and its arguments to be executed,
* a pointer to a struct termios `ttyOrig` representing the original terminal settings,
* a flag `context_override` indicating whether to override the process's execution context,
* a string `context_user` representing the username to use for the execution context if `context_override` is `true`,
* and a string `context_group` representing the group name to use for the execution context if `context_override` is `true`.
*
* The function first redirects the child process's stderr to the write end of the stderr pipe.
* If `context_override` is `true`, the function sets the process's execution context using `set_identity_context()`.
* If `context_override` is `false`, the function does nothing.
* Finally, the function executes the command specified in `processed_command` using `execvp()`.
* If the execution of `execvp()` fails, the function calls `safe_perror()` to print a message and exit the program.
*/
void run_child_process( int fd_child_stderr_pipe[2], char * processed_command[], struct termios * ttyOrig, bool context_override, std::string context_user, std::string context_group )
{
// redirect stderr to the write end of the stderr pipe
// close the file descriptor STDERR_FILENO if it was previously open, then (re)open it as a copy of
while ((dup2(fd_child_stderr_pipe[WRITE_END], STDERR_FILENO) == -1) && (errno == EINTR)) {}
// close the write end of the stderr pipe
close( fd_child_stderr_pipe[WRITE_END] );
// if the user has specified a context override, set the context
if ( context_override )
{
int context_result = set_identity_context(context_user, context_group);
switch (context_result) {
case IDENTITY_CONTEXT_ERRORS::ERROR_NONE:
break;
case IDENTITY_CONTEXT_ERRORS::ERROR_NO_SUCH_USER:
std::cerr << "lcpex: Aborting: context user not found: " << context_user << std::endl;
exit(1);
break;
case IDENTITY_CONTEXT_ERRORS::ERROR_NO_SUCH_GROUP:
std::cerr << "lcpex: Aborting: context group not found: " << context_group << std::endl;
exit(1);
break;
case IDENTITY_CONTEXT_ERRORS::ERROR_SETGID_FAILED:
std::cerr << "lcpex: Aborting: Setting GID failed: " << context_user << "/" << context_group << std::endl;
exit(1);
break;
case IDENTITY_CONTEXT_ERRORS::ERROR_SETUID_FAILED:
std::cerr << "lcpex: Aborting: Setting UID failed: " << context_user << "/" << context_group << std::endl;
exit(1);
break;
default:
std::cerr << "lcpex: Aborting: Unknown error while setting identity context." << std::endl;
exit(1);
break;
}
}
// execute the dang command, print to stdout, stderr (of parent), and dump to file for each!!!!
// (and capture exit code in parent)
int exit_code = execvp(processed_command[0], processed_command);
safe_perror("failed on execvp in child", ttyOrig );
exit(exit_code);
}
// this does three things:
// - execute a dang string as a subprocess command
// - capture child stdout/stderr to respective log files
// - TEE child stdout/stderr to parent stdout/stderr
int exec_pty(
std::string command,
std::string stdout_log_file,
std::string stderr_log_file,
bool context_override,
std::string context_user,
std::string context_group,
bool environment_supplied
) {
// initialize the terminal settings obj
struct termios ttyOrig;
// if the user chose to supply the environment, then we need to clear the environment
// before we fork the process, so that the child process will inherit the environment
// from the parent process
if ( environment_supplied ) {
clearenv();
}
// turn our command string into something execvp can consume
char ** processed_command = expand_env(command );
// open file handles to the two log files we need to create for each execution
FILE * stdout_log_fh = fopen( stdout_log_file.c_str(), "a+" );
FILE * stderr_log_fh = fopen( stderr_log_file.c_str(), "a+" );
// create the pipes for the child process to write and read from using its stderr
int fd_child_stderr_pipe[2];
if ( pipe( fd_child_stderr_pipe ) == -1 ) {
safe_perror( "child stderr pipe", &ttyOrig );
exit( 1 );
}
// using O_CLOEXEC to ensure that the child process closes the file descriptors
if ( fcntl( fd_child_stderr_pipe[READ_END], F_SETFD, FD_CLOEXEC ) == -1 ) { perror("fcntl"); exit(1); }
// // same for stderr write
if ( fcntl( fd_child_stderr_pipe[WRITE_END], F_SETFD, FD_CLOEXEC ) == -1 ) { perror("fcntl"); exit(1); }
// status result basket for the parent process to capture the child's exit status
int status = 616;
// start ptyfork integration
char slaveName[MAX_SNAME];
int masterFd;
struct winsize ws;
/* Retrieve the attributes of terminal on which we are started */
if (tcgetattr(STDIN_FILENO, &ttyOrig) == -1)
safe_perror("tcgetattr", &ttyOrig);
if (ioctl(STDIN_FILENO, TIOCGWINSZ, &ws) < 0)
safe_perror("ioctl-TIOCGWINSZ", &ttyOrig );
pid_t pid = ptyFork( &masterFd, slaveName, MAX_SNAME, &ttyOrig, &ws );
switch( pid ) {
case -1:
{
// fork failed
safe_perror("ptyfork failure", &ttyOrig );
exit(1);
}
case 0:
{
// child process
run_child_process( fd_child_stderr_pipe, processed_command, &ttyOrig, context_override, context_user, context_group );
}
default:
{
// parent process
// start ptyfork integration
ttySetRaw(STDIN_FILENO, &ttyOrig);
// The parent process has no need to access the entrance to the pipe
close(fd_child_stderr_pipe[WRITE_END]);
// attempt to write to stdout,stderr from child as well as to write each to file
char buf[BUFFER_SIZE];
// contains the byte count of the last read from the pipe
ssize_t byte_count;
// watched_fds for poll() to wait on
struct pollfd watched_fds[3];
// populate the watched_fds array
// child in to parent
watched_fds[0].fd = STDIN_FILENO;
watched_fds[0].events = POLLIN;
// child pty to parent (stdout)
watched_fds[1].fd = masterFd;
watched_fds[1].events = POLLIN;
// child stderr to parent
watched_fds[2].fd = fd_child_stderr_pipe[READ_END];
watched_fds[2].events = POLLIN;
// number of files poll() reports as ready
int num_files_readable;
// loop flag
bool break_out = false;
// loop until we've read all the data from the child process
while ( ! break_out ) {
num_files_readable = poll(watched_fds, sizeof(watched_fds) / sizeof(watched_fds[0]), -1);
if (num_files_readable == -1) {
// error occurred in poll()
safe_perror("poll", &ttyOrig );
exit(1);
}
if (num_files_readable == 0) {
// poll reports no files readable
break_out = true;
break;
}
for (int this_fd = 0; this_fd < 3; this_fd++) {
if (watched_fds[this_fd].revents & POLLIN) {
// this pipe is readable
byte_count = read(watched_fds[this_fd].fd, buf, BUFFER_SIZE);
if (byte_count == -1) {
// error reading from pipe
safe_perror("read", &ttyOrig );
exit(EXIT_FAILURE);
} else if (byte_count == 0) {
// reached EOF on one of the streams but not a HUP
// we've read all we can this cycle, so go to the next fd in the for loop
continue;
} else {
// byte count was sane
// write to stdout,stderr
if (this_fd == 0) {
// parent stdin received, write to child pty (stdin)
write(masterFd, buf, byte_count);
} else if (this_fd == 1 ) {
// child pty sent some stuff, write to parent stdout and log
write(stdout_log_fh->_fileno, buf, byte_count);
write(STDOUT_FILENO, buf, byte_count);
} else if ( this_fd == 2 ){
//the child's stderr pipe sent some stuff, write to parent stderr and log
write(stderr_log_fh->_fileno, buf, byte_count);
write(STDERR_FILENO, buf, byte_count);
} else {
// this should never happen
perror("Logic error!");
exit(EXIT_FAILURE);
}
}
}
if (watched_fds[this_fd].revents & POLLERR) {
close(watched_fds[this_fd].fd);
//break_out = true;
continue;
}
if (watched_fds[this_fd].revents & POLLHUP) {
// this pipe has hung up
close(watched_fds[this_fd].fd);
break_out = true;
break;
}
}
}
// wait for child to exit, capture status
waitpid(pid, &status, 0);
// close the log file handles
fclose(stdout_log_fh);
fclose(stderr_log_fh);
ttyResetExit( &ttyOrig);
if WIFEXITED(status) {
return WEXITSTATUS(status);
} else {
return -617;
}
}
}
}

View File

@ -0,0 +1,50 @@
//
// Created by phanes on 2/6/23.
//
#ifndef LCPEX_LIBCLPEX_TTY_H
#define LCPEX_LIBCLPEX_TTY_H
#include <cstdio>
#include <cstdlib>
#include <poll.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/wait.h>
#include <iostream>
#include "../string_expansion/string_expansion.h"
#include "../helpers.h"
#include "../Contexts.h"
#include "../vpty/pty_fork_mod/tty_functions.h"
#include "../vpty/pty_fork_mod/pty_fork.h"
#include <sys/ioctl.h>
#include <string>
/**
* @brief Execute a string as a subprocess command, capture its stdout/stderr to log files, and TEE its output to the parent process's stdout/stderr.
* This function performs the following three tasks:
* - Execute a string as a subprocess command.
* - Capture the child process's stdout and stderr to respective log files.
* - TEE the child process's stdout and stderr to the parent process's stdout and stderr.
*
* @param command The command to be executed as a subprocess.
* @param stdout_log_file The file path where the child process's stdout will be logged.
* @param stderr_log_file The file path where the child process's stderr will be logged.
* @param context_override Specify whether to override the process's execution context.
* @param context_user The user context to run the process as, if context_override is true.
* @param context_group The group context to run the process as, if context_override is true.
* @param environment_supplied Specify whether the environment is supplied.
* @return The exit status of the child process. If the child process terminated due to a signal, returns -617.
*/
int exec_pty(
std::string command,
std::string stdout_log_file,
std::string stderr_log_file,
bool context_override,
std::string context_user,
std::string context_group,
bool environment_supplied
);
#endif //LCPEX_LIBCLPEX_TTY_H

View File

@ -0,0 +1,79 @@
#include "pty_fork.h"
pid_t ptyFork(int *masterFd, char *slaveName, size_t snLen,
const struct termios *slaveTermios, const struct winsize *slaveWS)
{
int mfd, slaveFd, savedErrno;
pid_t childPid;
char slname[MAX_SNAME];
mfd = ptyMasterOpen(slname, MAX_SNAME);
if (mfd == -1)
return -1;
if (slaveName != NULL) { /* Return slave name to caller */
if (strlen(slname) < snLen) {
strncpy(slaveName, slname, snLen);
} else { /* 'slaveName' was too small */
close(mfd);
errno = EOVERFLOW;
return -1;
}
}
childPid = fork();
/* fork() failed */
if (childPid == -1) {
/* close() might change 'errno' */
savedErrno = errno;
/* Don't leak file descriptors */
close(mfd);
errno = savedErrno;
return -1;
}
if (childPid != 0) { /* Parent */
*masterFd = mfd; /* Only parent gets master fd */
return childPid; /* Like parent of fork() */
}
/* Child falls through to here */
if (setsid() == -1) /* Start a new session */
perror("ptyFork:setsid");
close(mfd); /* Not needed in child */
slaveFd = open(slname, O_RDWR); /* Becomes controlling tty */
if (slaveFd == -1)
perror("ptyFork:open-slave");
#ifdef TIOCSCTTY /* Acquire controlling tty on BSD */
if (ioctl(slaveFd, TIOCSCTTY, 0) == -1)
perror("ptyFork:ioctl-TIOCSCTTY");
#endif
if (slaveTermios != NULL) /* Set slave tty attributes */
if (tcsetattr(slaveFd, TCSANOW, slaveTermios) == -1)
perror("ptyFork:tcsetattr");
if (slaveWS != NULL) /* Set slave tty window size */
if (ioctl(slaveFd, TIOCSWINSZ, slaveWS) == -1)
perror("ptyFork:ioctl-TIOCSWINSZ");
/* Duplicate pty slave to be child's stdin, stdout, and stderr */
if (dup2(slaveFd, STDIN_FILENO) != STDIN_FILENO)
perror("ptyFork:dup2-STDIN_FILENO");
if (dup2(slaveFd, STDOUT_FILENO) != STDOUT_FILENO)
perror("ptyFork:dup2-STDOUT_FILENO");
if (dup2(slaveFd, STDERR_FILENO) != STDERR_FILENO)
perror("ptyFork:dup2-STDERR_FILENO");
if (slaveFd > STDERR_FILENO) /* Safety check */
close(slaveFd); /* No longer need this fd */
return 0; /* Like child of fork() */
}

View File

@ -0,0 +1,39 @@
#ifndef LCPEX_PTY_FORK_H
#define LCPEX_PTY_FORK_H
#include <sys/types.h>
#include "./pty_master_open.h"
#include <cstdio>
#include "./tty_functions.h"
#include <sys/ioctl.h>
/* Maximum size for pty slave name */
#define MAX_SNAME 1000
/**
* @brief Fork a child process with a pseudo-terminal
*
* This function forks a child process and returns the child's process ID. The
* child process is attached to a pseudo-terminal and its standard input,
* output, and error streams are redirected to the slave side of the pseudo-terminal.
*
* @param[out] masterFd Pointer to store the file descriptor of the master pty
* @param[out] slaveName Buffer to store the name of the slave pty
* @param[in] snLen Length of the 'slaveName' buffer
* @param[in] slaveTermios Terminal attributes for the slave pty (optional)
* @param[in] slaveWS Window size for the slave pty (optional)
*
* @return Process ID of the child process on success, or -1 on error
*
* On success, the file descriptor of the master pty is stored in the location
* pointed to by 'masterFd', and the name of the slave pty is stored in the buffer
* pointed to by 'slaveName'. If the buffer is too small to hold the name of the
* slave, an error of type 'EOVERFLOW' is returned. If 'slaveTermios' is non-NULL,
* the terminal attributes specified by it will be set for the slave pty. If
* 'slaveWS' is non-NULL, the window size specified by it will be set for the slave
* pty.
*/
pid_t ptyFork(int *masterFd, char *slaveName, size_t snLen, const struct termios *slaveTermios, const struct winsize *slaveWS);
#endif //LCPEX_PTY_FORK_H

View File

@ -0,0 +1,76 @@
/* pty_master_open.cPP
Implement our ptyMasterOpen() function, based on UNIX 98 pseudoterminals.
See comments below.
See also pty_master_open_bsd.c.
*/
#if ! defined(__sun)
/* Prevents ptsname() declaration being visible on Solaris 8 */
#if ! defined(_XOPEN_SOURCE) || _XOPEN_SOURCE < 600
#define _XOPEN_SOURCE 600
#endif
#endif
#include "pty_master_open.h" /* Declares ptyMasterOpen() */
/* Some implementations don't have posix_openpt() */
#if defined(__sun) /* Not on Solaris 8 */
#define NO_POSIX_OPENPT
#endif
#ifdef NO_POSIX_OPENPT
static int
posix_openpt(int flags)
{
return open("/dev/ptmx", flags);
}
#endif
/* Open a pty master, returning file descriptor, or -1 on error.
On successful completion, the name of the corresponding pty
slave is returned in 'slaveName'. 'snLen' should be set to
indicate the size of the buffer pointed to by 'slaveName'. */
int ptyMasterOpen(char *slaveName, size_t snLen)
{
int masterFd, savedErrno;
char *p;
masterFd = posix_openpt(O_RDWR | O_NOCTTY); /* Open pty master */
if (masterFd == -1)
return -1;
if (grantpt(masterFd) == -1) { /* Grant access to slave pty */
savedErrno = errno;
close(masterFd); /* Might change 'errno' */
errno = savedErrno;
return -1;
}
if (unlockpt(masterFd) == -1) { /* Unlock slave pty */
savedErrno = errno;
close(masterFd); /* Might change 'errno' */
errno = savedErrno;
return -1;
}
p = ptsname(masterFd); /* Get slave pty name */
if (p == nullptr) {
savedErrno = errno;
close(masterFd); /* Might change 'errno' */
errno = savedErrno;
return -1;
}
if (strlen(p) < snLen) {
strncpy(slaveName, p, snLen);
} else { /* Return an error if buffer too small */
close(masterFd);
errno = EOVERFLOW;
return -1;
}
return masterFd;
}

View File

@ -0,0 +1,32 @@
#ifndef LCPEX_PTY_MASTER_OPEN_H
#define LCPEX_PTY_MASTER_OPEN_H
#include <sys/types.h>
#include <errno.h>
#include <cstdlib>
#include <fcntl.h>
#include <csignal>
#include <cstring>
#include <stdlib.h>
#include <fcntl.h>
/**
* @brief Open a pseudo-terminal master
*
* This function opens a pseudo-terminal master and returns the file descriptor
* for the master. The name of the corresponding pseudo-terminal slave is also
* returned in the buffer pointed to by 'slaveName'.
*
* @param[out] slaveName Buffer to store the name of the slave pty
* @param[in] snLen Length of the 'slaveName' buffer
*
* @return File descriptor of the master pty on success, or -1 on error
*
* On success, the name of the corresponding pseudo-terminal slave is stored in
* the buffer pointed to by 'slaveName'. If the buffer is too small to hold the
* name of the slave, an error of type 'EOVERFLOW' is returned.
*/
int ptyMasterOpen(char *slaveName, size_t snLen);
#endif //LCPEX_PTY_MASTER_OPEN_H

View File

@ -0,0 +1,77 @@
#include "tty_functions.h"
/* Reset terminal mode on program exit */
void ttyResetExit( struct termios * ttyOrig )
{
if (tcsetattr(STDIN_FILENO, TCSAFLUSH, ttyOrig) == -1)
perror("tcsetattr");
}
/* Place terminal referred to by 'fd' in cbreak mode (noncanonical mode
with echoing turned off). This function assumes that the terminal is
currently in cooked mode (i.e., we shouldn't call it if the terminal
is currently in raw mode, since it does not undo all of the changes
made by the ttySetRaw() function below). Return 0 on success, or -1
on error. If 'prevTermios' is non-NULL, then use the buffer to which
it points to return the previous terminal settings. */
int ttySetCbreak(int fd, struct termios *prevTermios)
{
struct termios t;
if (tcgetattr(fd, &t) == -1)
return -1;
if (prevTermios != nullptr)
*prevTermios = t;
t.c_lflag &= ~(ICANON | ECHO);
t.c_lflag |= ISIG;
t.c_iflag &= ~ICRNL;
t.c_cc[VMIN] = 1; /* Character-at-a-time input */
t.c_cc[VTIME] = 0; /* with blocking */
if (tcsetattr(fd, TCSAFLUSH, &t) == -1)
return -1;
return 0;
}
/* Place terminal referred to by 'fd' in raw mode (noncanonical mode
with all input and output processing disabled). Return 0 on success,
or -1 on error. If 'prevTermios' is non-NULL, then use the buffer to
which it points to return the previous terminal settings. */
int ttySetRaw(int fd, struct termios *prevTermios)
{
struct termios t;
if (tcgetattr(fd, &t) == -1)
return -1;
if (prevTermios != nullptr)
*prevTermios = t;
t.c_lflag &= ~(ICANON | ISIG | IEXTEN | ECHO);
/* Noncanonical mode, disable signals, extended
input processing, and echoing */
t.c_iflag &= ~(BRKINT | ICRNL | IGNBRK | IGNCR | INLCR |
INPCK | ISTRIP | IXON | PARMRK);
/* Disable special handling of CR, NL, and BREAK.
No 8th-bit stripping or parity error handling.
Disable START/STOP output flow control. */
// this breaks cursor position change tracking in parent processes
//t.c_oflag &= ~OPOST; /* Disable all output processing */
t.c_cc[VMIN] = 1; /* Character-at-a-time input */
t.c_cc[VTIME] = 0; /* with blocking */
if (tcsetattr(fd, TCSAFLUSH, &t) == -1)
return -1;
return 0;
}

View File

@ -0,0 +1,59 @@
#ifndef LCPEX_TTY_FUNCTIONS_H
#define LCPEX_TTY_FUNCTIONS_H
#include <termios.h>
#include <cstdio>
#include <iostream>
#include <unistd.h>
/**
* @brief Place terminal referred to by 'fd' in cbreak mode
*
* This function places the terminal referred to by the file descriptor 'fd'
* in cbreak mode (noncanonical mode with echoing turned off). It assumes that
* the terminal is currently in cooked mode (i.e., it should not be called
* if the terminal is currently in raw mode, since it does not undo all of
* the changes made by the ttySetRaw() function).
*
* @param fd File descriptor of the terminal
* @param prevTermios Buffer to store the previous terminal settings
*
* @return 0 on success, or -1 on error
*
* If 'prevTermios' is non-NULL, then the buffer pointed to by it will be used
* to return the previous terminal settings.
*/
int ttySetCbreak(int fd, struct termios * prevTermios);
/**
* @brief Place terminal referred to by 'fd' in raw mode
*
* This function places the terminal referred to by the file descriptor 'fd'
* in raw mode.
*
* @param fd File descriptor of the terminal
* @param prevTermios Buffer to store the previous terminal settings
*
* @return 0 on success, or -1 on error
*
* If 'prevTermios' is non-NULL, then the buffer pointed to by it will be used
* to return the previous terminal settings.
*/
int ttySetRaw(int fd, struct termios * prevTermios);
/**
* @brief Reset terminal mode on program exit
*
* @param ttyOrig Original terminal mode to be restored
*
* This function resets the terminal mode when the program exits by using the
* tcsetattr function. If the tcsetattr function returns an error, the perror
* function is called to print an error message.
*/
void ttyResetExit( struct termios * ttyOrig );
#endif //LCPEX_TTY_FUNCTIONS_H

View File

@ -1,144 +0,0 @@
/*
Rex - A configuration management and workflow automation tool that
compiles and runs in minimal environments.
© SILO GROUP and Chris Punches, 2020.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "Conf.h"
/// ConfigLoadException - General exception handler for the Conf class.
class ConfigLoadException: public std::exception
{
public:
/** Constructor (C strings).
* @param message C-style string error message.
* The string contents are copied upon construction.
* Hence, responsibility for deleting the char* lies
* with the caller.
*/
explicit ConfigLoadException(const char* message):
msg_(message)
{
}
/** Constructor (C++ STL strings).
* @param message The error message.
*/
explicit ConfigLoadException(const std::string& message):
msg_(message)
{}
/** Destructor.
* Virtual to allow for subclassing.
*/
virtual ~ConfigLoadException() throw (){}
/** Returns a pointer to the (constant) error description.
* @return A pointer to a const char*. The underlying memory
* is in posession of the Exception object. Callers must
* not attempt to free the memory.
*/
virtual const char* what() const throw (){
return msg_.c_str();
}
protected:
/** Error message.
*/
std::string msg_;
};
/// Conf::Conf - Constructor for Conf type. Loads the configuration for the application.
/// TODO Expand to detect when a directory path is supplied for units_path or plan_path and import all Tasks and Units.
///
/// \param filename - The filename to load the configuration from.
Conf::Conf(std::string filename, int LOG_LEVEL ): JSON_Loader(LOG_LEVEL ), slog(LOG_LEVEL, "_conf_" )
{
this->LOG_LEVEL = LOG_LEVEL;
std::string jval_s;
bool jval_b;
// prepare context spaghetti
this->override_context = false;
interpolate( filename);
try {
// load the test file.
this->load_json_file( filename );
}
catch (std::exception) {
this->slog.log( E_FATAL, "Unable to locate configuration file: '" + filename + "'." );
throw ConfigLoadException("Config file not found.");
}
// find the path to the unit definitions file
if (this->get_string(jval_s, "units_path") != 0 )
{ throw ConfigLoadException("units_path string is not set in the config file supplied: " + filename); } else {
interpolate( jval_s );
this->units_path = jval_s;
}
jval_s = {0};
// find the path to logs directory
if (this->get_string(jval_s, "logs_path") != 0 )
{ throw ConfigLoadException("logs_path string is not set in the config file supplied: " + filename); } else {
interpolate( jval_s );
this->logs_path = jval_s;
}
jval_s = {0};
if (this->get_bool(jval_b, "execution_context_override") != 0 )
{ throw ConfigLoadException("execution_context_override boolean is not set in the config file supplied: " + filename); } else {
this->override_context = jval_b;
}
jval_b = {0};
if (this->get_string(jval_s, "execution_context") != 0 )
{ throw ConfigLoadException("execution_context string is not set in the config file supplied: " + filename); } else {
interpolate( jval_s );
if ( ! is_dir( jval_s ) ) { throw ConfigLoadException( "The execution context supplied is an invalid directory."); } else {
this->execution_context = jval_s;
}
}
jval_s = {0};
};
/// Conf::has_context_override - Specifies whether or not the override context function is enabled in the Conf file.
bool Conf::has_context_override() {
return this->override_execution_context;
}
/// Conf::get_execution_context - Specifies the path to the current working directory to set for all unit executions.
std::string Conf::get_execution_context() {
return this->execution_context;
}
/// Conf::get_units_path - Retrieves the path to the Unit definition file from the application configuration file.
std::string Conf::get_units_path() { return this->units_path; }
/// Conf::get_units_path - Retrieves the path to the Unit definition file from the application configuration file.
std::string Conf::get_logs_path() { return this->logs_path; }
/// Conf::set_execution_context- Sets the execution context.
void Conf::set_execution_context(std::string execution_context )
{
this->execution_context = execution_context;
}

View File

@ -1,66 +0,0 @@
/*
Rex - A configuration management and workflow automation tool that
compiles and runs in minimal environments.
© SILO GROUP and Chris Punches, 2020.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef REX_CONF_H
#define REX_CONF_H
#include "../low_level/JSON_Loader.h"
#include <exception>
#include "../../Logger/Logger.h"
#include "../misc/helpers.h"
#define STRINGIZE2(s) #s
#define STRINGIZE(s) STRINGIZE2(s)
# define IMPL_CONFIG_VERSION 5
# define VERSION_STRING STRINGIZE(IMPL_CONFIG_VERSION)
class Conf: public JSON_Loader
{
private:
std::string plan_path;
std::string units_path;
std::string execution_context;
std::string logs_path;
// flag to indicate if execution context should be overriden in config file
// if set to true rex should use whats in the config file for current working directory
// if set to false, rex should use the current working directory at time of execution
bool override_execution_context;
bool override_context;
public:
Conf( std::string filename, int LOG_LEVEL );
bool has_context_override();
std::string get_units_path();
std::string get_execution_context();
std::string get_logs_path();
void set_execution_context( std::string );
private:
int LOG_LEVEL;
Logger slog;
};
#endif //REX_CONF_H

View File

@ -1,276 +0,0 @@
/*
Rex - A configuration management and workflow automation tool that
compiles and runs in minimal environments.
© SILO GROUP and Chris Punches, 2020.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "Plan.h"
/// Plan_InvalidTaskIndex - Exception thrown when a Plan tries to access a contained Task's value by index not present
/// in the Unit.
class Plan_InvalidTaskIndex: public std::runtime_error { public:
Plan_InvalidTaskIndex(): std::runtime_error("Plan: Attempted to access a Task using an invalid index.") {}
};
/// Plan_InvalidTaskName - Exception thrown when a Plan tries to access a contained Task's value by name not present
/// in the Unit.
class Plan_InvalidTaskName: public std::runtime_error { public:
Plan_InvalidTaskName(): std::runtime_error("Plan: Attempted to access a Task using an invalid name.") {}
};
/// Plan_Task_GeneralExecutionException - Wrapper exception to catch exceptions thrown by the execution of Tasks.
class Plan_Task_GeneralExecutionException: public std::exception
{
public:
/** Constructor (C strings).
* @param message C-style string error message.
* The string contents are copied upon construction.
* Hence, responsibility for deleting the char* lies
* with the caller.
*/
explicit Plan_Task_GeneralExecutionException(const char* message):
msg_(message)
{
}
/** Constructor (C++ STL strings).
* @param message The error message.
*/
explicit Plan_Task_GeneralExecutionException(const std::string& message):
msg_(message)
{}
/** Destructor.
* Virtual to allow for subclassing.
*/
virtual ~Plan_Task_GeneralExecutionException() throw (){}
/** Returns a pointer to the (constant) error description.
* @return A pointer to a const char*. The underlying memory
* is in posession of the Exception object. Callers must
* not attempt to free the memory.
*/
virtual const char* what() const throw (){
return msg_.c_str();
}
protected:
/** Error message.
*/
std::string msg_;
};
/// Plan_Task_Missing_Dependency - Exception thrown when a Plan tries to access a contained Task's value by name not present
/// in the Unit.
class Plan_Task_Missing_Dependency: public std::exception
{
public:
/** Constructor (C strings).
* @param message C-style string error message.
* The string contents are copied upon construction.
* Hence, responsibility for deleting the char* lies
* with the caller.
*/
explicit Plan_Task_Missing_Dependency(const char* message):
msg_(message)
{
}
/** Constructor (C++ STL strings).
* @param message The error message.
*/
explicit Plan_Task_Missing_Dependency(const std::string& message):
msg_(message)
{}
/** Destructor.
* Virtual to allow for subclassing.
*/
virtual ~Plan_Task_Missing_Dependency() throw (){}
/** Returns a pointer to the (constant) error description.
* @return A pointer to a const char*. The underlying memory
* is in posession of the Exception object. Callers must
* not attempt to free the memory.
*/
virtual const char* what() const throw (){
return msg_.c_str();
}
protected:
/** Error message.
*/
std::string msg_;
};
/// Plan::Plan() - Constructor for Plan class. A Plan is a managed container for a Task vector. These tasks reference
/// Units that are defined in the Units files (Suite). If Units are definitions, Tasks are selections of those
/// definitions to execute, and if Units together form a Suite, Tasks together form a Plan.
Plan::Plan(Conf * configuration, int LOG_LEVEL ): JSON_Loader(LOG_LEVEL ), slog(LOG_LEVEL, "_plan_" )
{
this->configuration = configuration;
this->LOG_LEVEL = LOG_LEVEL;
}
/// Plan::load_plan_file - Uses the json_root buffer on each run to append intact Units as they're deserialized from
/// the provided file.
///
/// \param filename - The filename to load the plan from.
/// \param verbose - Whether to print verbose output to STDOUT.
void Plan::load_plan_file( std::string filename )
{
// plan always loads from file
this->load_json_file( filename );
// staging buffer
Json::Value jbuff;
// fill the jbuff staging buffer wih a json::value object in the supplied filename
if ( this->get_serialized( jbuff, "plan" ) == 0 )
{
this->json_root = jbuff;
}
// iterate through the json::value members that have been loaded. append to this->tasks vector
// buffer for tasks to append:
Task tmp_T = Task( this->LOG_LEVEL );
for ( int index = 0; index < this->json_root.size(); index++ )
{
tmp_T.load_root( this->json_root[ index ] );
this->tasks.push_back( tmp_T );
this->slog.log( LOG_INFO, "Added task \"" + tmp_T.get_name() + "\" to Plan." );
}
}
/// Plan::get_task - Retrieves a task by index.
///
/// \param result - The variable receiving the value.
/// \param index - The numerical index in the Task vector to retrieve a value for.
/// \param verbose - Whether to print verbose output to STDOUT.
void Plan::get_task(Task & result, int index )
{
if ( index <= this->tasks.size() )
{
result = this->tasks[ index ];
} else {
throw Plan_InvalidTaskIndex();
}
}
/// Plan::load_definitions - Load the units corresponding to each task in plan from the given Suite.
///
/// \param unit_definitions - The Suite to load definitions from.
/// \param verbose - Whether to print verbose information to STDOUT.
void Plan::load_definitions( Suite unit_definitions )
{
// placeholder Unit
Unit tmp_U = Unit( this->LOG_LEVEL );
// for every task in the plan:
for (int i = 0; i < this->tasks.size(); i++ )
{
// load the tmp_U corresponding to that task name
unit_definitions.get_unit( tmp_U, this->tasks[i].get_name() );
// then have that task attach a copy of tmp_U
this->tasks[i].load_definition( tmp_U );
}
}
/// Plan::get_task - Retrieves a task by name.
///
/// \param result - The variable receiving the value.
/// \param provided_name - The name to find a task by.
/// \param verbose - Whether to print verbose output to STDOUT.
void Plan::get_task(Task & result, std::string provided_name )
{
bool foundMatch = false;
for ( int i = 0; i < this->tasks.size(); i++ )
{
if ( this->tasks[i].get_name() == provided_name )
{
result = this->tasks[i];
foundMatch = true;
break;
}
}
if (! foundMatch )
{
this->slog.log( E_FATAL, "Task name \"" + provided_name + "\" was referenced but not defined!" );
throw Plan_InvalidTaskName();
}
}
/// Plan::all_dependencies_complete
///
/// \param name - The name of the task in the plan to check met dependencies for.
/// \return - boolean representation of whether all dependencies are complete or not.
bool Plan::all_dependencies_complete(std::string name)
{
// get the task by name
Task named_task = Task( this->LOG_LEVEL );
this->get_task( named_task, name );
// get the dependencies of that task
std::vector<std::string> deps = named_task.get_dependencies();
// create an empty task to assign values to during iteration
Task tmpTask = Task( this->LOG_LEVEL );
// iterate through its dependencies
for ( int i = 0; i < deps.size(); i++ )
{
this->get_task( tmpTask, deps[i]);
if (! tmpTask.is_complete() )
{
// error message?
return false;
}
}
return true;
}
/// Plan::execute() - Iterates through all tasks in a plan and executes them.
///
/// \param verbose
void Plan::execute()
{
// for each task in this plan
for ( int i = 0; i < this->tasks.size(); i++ )
{
if (this->all_dependencies_complete(this->tasks[i].get_name()) )
{
this->slog.log( E_INFO, "[ '" + this->tasks[i].get_name() + "' ] Executing..." );
try {
this->tasks[i].execute( this->configuration );
}
catch (std::exception& e) {
this->slog.log( E_FATAL, "[ '" + this->tasks[i].get_name() + "' ] Report: " + e.what() );
throw Plan_Task_GeneralExecutionException("Could not execute task.");
}
} else {
// not all deps met for this task
this->slog.log( E_FATAL, "[ '" + this->tasks[i].get_name() + "' ] This task was specified in the Plan but not executed due to missing dependencies. Please revise your plan." );
throw Plan_Task_Missing_Dependency( "Unmet dependency for task." );
}
}
}

View File

@ -1,67 +0,0 @@
/*
Rex - A configuration management and workflow automation tool that
compiles and runs in minimal environments.
© SILO GROUP and Chris Punches, 2020.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef REX_PLAN_H
#define REX_PLAN_H
#include <string>
#include "../../json/json.h"
#include "../low_level/JSON_Loader.h"
#include "Task.h"
#include "Conf.h"
#include "../../Logger/Logger.h"
class Plan: public JSON_Loader
{
private:
// storage for the tasks that make up the plan
std::vector<Task> tasks;
Conf * configuration;
public:
Plan(Conf * configuration, int LOG_LEVEL );
// append this->tasks from JSON file
void load_plan_file( std::string filename );
// fetch a task from this->tasks
void get_task( Task & result, std::string provided_name );
// fetch a task from this->tasks
void get_task( Task & result, int index );
// load unit definitions from a provided suite and import them into individual tasks
void load_definitions( Suite unit_definitions );
// fetch a corresponding Unit to a Task
// void get_definition_from_task(Unit & result, Task input, bool verbose );
// execute all tasks in this plan
void execute();
bool all_dependencies_complete(std::string name);
private:
int LOG_LEVEL;
Logger slog;
};
#endif //REX_PLAN_H

View File

@ -1,58 +0,0 @@
/*
Rex - A configuration management and workflow automation tool that
compiles and runs in minimal environments.
© SILO GROUP and Chris Punches, 2020.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef REX_UNITS_H
#define REX_UNITS_H
#include <vector>
#include "../../json/json.h"
#include "../low_level/JSON_Loader.h"
#include "Unit.h"
#include "../../Logger/Logger.h"
#include "../misc/helpers.h"
class Suite: public JSON_Loader
{
protected:
// storage for the definitions we are amassing from the unit definition files
std::vector<Unit> units;
public:
// constructor
Suite( int LOG_LEVEL );
// load a unit definitions file and add valid unit definitions to this->units
void load_units_file( std::string filename );
// returns the unit identified by name
void get_unit(Unit & result, std::string provided_name);
private:
void get_units_from_dir( std::vector<std::string> * files, std::string path );
private:
int LOG_LEVEL;
Logger slog;
};
#endif //REX_UNITS_H

View File

@ -1,396 +0,0 @@
/*
Rex - A configuration management and workflow automation tool that
compiles and runs in minimal environments.
© SILO GROUP and Chris Punches, 2020.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "Task.h"
/// Task_InvalidDataStructure - Exception thrown when a Task is defined with invalid JSON.
class Task_InvalidDataStructure: public std::runtime_error {
public:
Task_InvalidDataStructure(): std::runtime_error("Task: Attempted to access a member of a Task that is not set.") {}
};
/// Task_InvalidDataStructure - Exception thrown when a Task is defined with invalid JSON.
class Task_NotReady: public std::runtime_error {
public:
Task_NotReady(): std::runtime_error("Task: Attempted to execute a Task whose Unit is not well defined.") {}
};
/// Task_RequiredButFailedTask - Exception thrown when a Task fails but should not.
class TaskException: public std::exception
{
public:
/** Constructor (C strings).
* @param message C-style string error message.
* The string contents are copied upon construction.
* Hence, responsibility for deleting the char* lies
* with the caller.
*/
explicit TaskException(const char* message):
msg_(message)
{
}
/** Constructor (C++ STL strings).
* @param message The error message.
*/
explicit TaskException(const std::string& message):
msg_(message)
{}
/** Destructor.
* Virtual to allow for subclassing.
*/
virtual ~TaskException() throw (){}
/** Returns a pointer to the (constant) error description.
* @return A pointer to a const char*. The underlying memory
* is in posession of the Exception object. Callers must
* not attempt to free the memory.
*/
virtual const char* what() const throw (){
return msg_.c_str();
}
protected:
/** Error message.
*/
std::string msg_;
};
/// Task::Task() - Constructor for the Task class. The Task is the building block of a Plan indicating of which Unit to
/// execute, and its dependencies on other units to have already been completed successfully.
Task::Task( int LOG_LEVEL ):
slog( LOG_LEVEL, "_task_" ),
definition( LOG_LEVEL )
{
// it hasn't executed yet.
this->complete = false;
// it hasn't been matched with a definition yet.
this->defined = false;
this->LOG_LEVEL = LOG_LEVEL;
}
/// Task::load_root() - loads json values to private members
///
/// \param loader_root - the Json::Value to populate from.
/// \param verbose - Whether to print verbose information to STDOUT.
void Task::load_root(Json::Value loader_root )
{
if ( loader_root.isMember("name") ) {
this->name = loader_root.get("name", "?").asString();
}
else {
throw Task_InvalidDataStructure();
}
// fetch as Json::Value array obj
Json::Value des_dep_root = loader_root.get("dependencies", 0);
// iterate through each member of that obj
for ( int i = 0; i < des_dep_root.size(); i++ ) {
// add each string to dependencies
if ( des_dep_root[i].asString() != "" )
{
this->dependencies.push_back( des_dep_root[i].asString() );
this->slog.log( E_INFO, "Added dependency \"" + des_dep_root[i].asString() + "\" to task \"" + this->get_name() + "\"." );
}
}
}
/// Task::get_name - Retrieves the name of the current Task.
std::string Task::get_name()
{
return this->name;
}
/// Task::load_definition - Loads a unit to a local member. Used to tie Units to Tasks.
///
/// \param selected_unit - The unit to attach.
/// \param verbose - Whether to print to STDOUT.
void Task::load_definition( Unit selected_unit )
{
this->definition = selected_unit;
this->slog.log( E_INFO, "Loaded definition \"" + selected_unit.get_name() + "\" as task in configured plan.");
this->defined = true;
}
/// Task::is_complete - Indicator if the task executed successfully.
bool Task::is_complete()
{
return this->complete;
}
/// Task::mark_complete - Marks the task complete..
void Task::mark_complete()
{
this->complete = true;
}
/// Task::get_dependencies - returns a pointer to the dependencies vector.
std::vector<std::string> Task::get_dependencies()
{
return this->dependencies;
}
/// Task::has_definition - Indicator if the task has attached its definition from a Suite.
bool Task::has_definition()
{
return this->defined;
}
/// Task::execute - execute a task's unit definition.
/// See the design document for what flow control needs to look like here.
/// \param verbose - Verbosity level - not implemented yet.
void Task::execute( Conf * configuration )
{
// DUFFING - If rex is broken it's probably going to be in this block.
// Somebody come clean this up, eh?
// PREWORK
// throw if unit not coupled to all necessary values since Task is stateful (yes, stateful is okay)
std::ostringstream infostring;
if ( ! this->has_definition() )
{
throw Task_NotReady();
}
// get the name
std::string task_name = this->definition.get_name();
this->slog.log( E_DEBUG, "[ '" + task_name + "' ] Using unit definition: \"" + task_name + "\"." );
// END PREWORK
// get the target execution command
std::string target_command = configuration->get_execution_context() + "/" + this->definition.get_target();
// check if context override
if ( configuration->has_context_override() )
{
// if so, set the CWD.
chdir( configuration->get_execution_context().c_str() );
this->slog.log( E_INFO, "[ '" + task_name + "' ] Setting execution context: " + get_working_path() );
}
// a[0] execute target
// TODO revise variable sourcing strategy
// ....sourcing on the shell for variables and environment population doesn't have a good smell.
this->slog.log( E_INFO, "[ '" + task_name + "' ] Executing target: \"" + target_command + "\"." );
std::string delimiter = " ";
int space_index = target_command.find( delimiter );
std::string bin_path = target_command.substr( 0, space_index );
if ( exists( bin_path ) )
{
this->slog.log( E_DEBUG, "[ '" + task_name + "' ] Target executable found.");
} else {
this->slog.log( E_FATAL, "[ '" + task_name + "' ] Target executable does not exist." );
throw Task_NotReady();
}
std::string static_env_file = configuration->get_execution_context() + "/" + this->definition.get_env_vars_file();
this->slog.log( E_INFO, "[ '" + task_name + "' ] Vars file: " + static_env_file );
this->slog.log( E_INFO, "[ '" + task_name + "' ] Shell: " + this->definition.get_shell() );
int return_code = Sproc::execute(
this->definition.get_shell(),
static_env_file,
this->definition.get_user(),
this->definition.get_group(),
target_command,
this->LOG_LEVEL,
task_name,
this->definition.get_stdout_log_flag(),
configuration->get_logs_path()
);
// **********************************************
// d[0] Error Code Check
// **********************************************
if ( return_code == 0 )
{
// d[0].0 ZERO
this->slog.log( E_INFO, "[ '" + task_name + "' ] Target succeeded. Marking as complete." );
this->mark_complete();
// a[1] NEXT
return;
}
if ( return_code != 0 )
{
// d[0].1 NON-ZERO
this->slog.log( E_WARN, "[ '" + task_name + "' ] Target failed with exit code " + std::to_string( return_code ) + "." );
// **********************************************
// d[1] Rectify Check
// **********************************************
if (! this->definition.get_rectify() )
{
// d[1].0 FALSE
// **********************************************
// d[2] Required Check
// **********************************************
if (! this->definition.get_required() )
{
// d[2].0 FALSE
// a[2] NEXT
this->slog.log( E_INFO, "[ '" + task_name + "' ] This task is not required to continue the plan. Moving on." );
return;
} else {
// d[2].1 TRUE
// a[3] EXCEPTION
this->slog.log( E_FATAL, "[ '" + task_name + "' ] Task is required, and failed, and rectification is not enabled." );
throw TaskException( "Task failed: " + task_name );
}
// **********************************************
// end - d[2] Required Check
// **********************************************
}
if ( this->definition.get_rectify() )
{
// d[1].1 TRUE (Rectify Check)
this->slog.log( E_INFO, "[ " + task_name + " ] Rectification pattern is enabled." );
// a[4] Execute RECTIFIER
std::string rectifier_command = this->definition.get_rectifier();
this->slog.log( E_INFO, "[ '" + task_name + "' ] Executing rectification: " + rectifier_command + "." );
int rectifier_error = Sproc::execute(
this->definition.get_shell(),
static_env_file,
this->definition.get_user(),
this->definition.get_group(),
rectifier_command,
this->LOG_LEVEL,
task_name,
this->definition.get_stdout_log_flag(),
configuration->get_logs_path()
);
// **********************************************
// d[3] Error Code Check for Rectifier
// **********************************************
if ( rectifier_error != 0 )
{
// d[3].1 Non-Zero
this->slog.log( E_WARN, "[ '" + task_name + "' ] Rectification failed with exit code " + std::to_string( rectifier_error ) + "." );
// **********************************************
// d[4] Required Check
// **********************************************
if ( ! this->definition.get_required() ) {
// d[4].0 FALSE
// a[5] NEXT
this->slog.log( E_INFO, "[ '" + task_name + "' ] This task is not required to continue the plan. Moving on." );
return;
}
if ( this->definition.get_required() )
{
// d[4].1 TRUE
// a[6] EXCEPTION
this->slog.log( E_FATAL, "[ '" + task_name + "' ] Task is required, but failed, and rectification failed. Lost cause." );
throw TaskException( "Lost cause, task failure." );
}
// **********************************************
// end - d[4] Required Check
// **********************************************
}
// d[3] check exit code of rectifier
if ( rectifier_error == 0 )
{
// d[3].0 Zero
this->slog.log( E_INFO, "[ '" + task_name + "' ] Rectification returned successfully." );
// a[7] Re-execute Target
this->slog.log( E_INFO, "[ '" + task_name + "' ] Re-Executing target \"" + this->definition.get_target() + "\"." );
int retry_code = Sproc::execute(
this->definition.get_shell(),
static_env_file,
this->definition.get_user(),
this->definition.get_group(),
target_command,
this->LOG_LEVEL,
task_name,
this->definition.get_stdout_log_flag(),
configuration->get_logs_path()
);
// **********************************************
// d[5] Error Code Check
// **********************************************
if ( retry_code == 0 )
{
// d[5].0 ZERO
// a[8] NEXT
this->slog.log( E_INFO, "[ '" + task_name + "' ] Re-execution was successful." );
return;
} else {
// d[5].1 NON-ZERO
this->slog.log( E_WARN, "[ '" + task_name + "' ] Re-execution failed with exit code " + std::to_string( retry_code ) + "." );
// **********************************************
// d[6] Required Check
// **********************************************
if ( ! this->definition.get_required() )
{
// d[6].0 FALSE
// a[9] NEXT
this->slog.log( E_INFO, "This task is not required to continue the plan. Moving on." );
return;
}
if ( this->definition.get_required() )
{
// d[6].1 TRUE
// a[10] EXCEPTION
this->slog.log( E_FATAL, "[ '" + task_name + "' ] Task is required, and failed, then rectified but rectifier did not heal the condition causing the target to fail. Cannot proceed with Plan." );
throw TaskException( "Lost cause, task failure." );
}
// **********************************************
// end - d[6] Required Check
// **********************************************
}
}
}
// **********************************************
// end d[1] Rectify Check
// **********************************************
}
}

View File

@ -1,302 +0,0 @@
/*
Rex - A configuration management and workflow automation tool that
compiles and runs in minimal environments.
© SILO GROUP and Chris Punches, 2020.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "Unit.h"
#include <iostream>
#include <fstream>
#include <cstdlib>
#include <stdexcept>
#include <pwd.h>
#include <grp.h>
/// UnitException -
class UnitException: public std::exception
{
public:
/** Constructor (C strings).
* @param message C-style string error message.
* The string contents are copied upon construction.
* Hence, responsibility for deleting the char* lies
* with the caller.
*/
explicit UnitException(const char* message):
msg_(message)
{
}
/** Constructor (C++ STL strings).
* @param message The error message.
*/
explicit UnitException(const std::string& message):
msg_(message)
{}
/** Destructor.
* Virtual to allow for subclassing.
*/
virtual ~UnitException() throw (){}
/** Returns a pointer to the (constant) error description.
* @return A pointer to a const char*. The underlying memory
* is in posession of the Exception object. Callers must
* not attempt to free the memory.
*/
virtual const char* what() const throw (){
return msg_.c_str();
}
protected:
/** Error message.
*/
std::string msg_;
};
/// Unit::Unit - Constructor for Unit type. The Unit is a definition of an automation task. Each Unit has:
/// name, used for identification and retrieval.
/// target, which is the filepath of an executable to trigger.
/// output, which is the desired output of the execution of target to STDOUT in determinations of of success or failure
/// (in addition to 0|non-0 exit code). If the output is set to look for "0" then it uses the exit code.
///
/// There is also:
/// rectifier, which is the path to an executable in the event of a non-0 exit code or a failure to get the desired
/// output.
/// required, which is used as a flag to halt or continue if rectifier does not heal the system in such a way that
/// target can run successfully.
/// rectify, which is used as a flag to determine in the rectifier runs.
Unit::Unit( int LOG_LEVEL ): JSON_Loader( LOG_LEVEL ), slog( LOG_LEVEL, "_unit_" )
{
this->LOG_LEVEL;
}
/// Unit::load_root - Takes a JSON::Value and assigns the members to the Unit being populated.
///
/// \param loader_root - The JSON::Value object to use to populate unit from. Usually supplied as the Suite's buffer
/// member.
/// \return - Boolean representation of success or failure.
int Unit::load_root(Json::Value loader_root)
{
// TODO this needs reworked to have errmsg actually end up as a null return from json::value.get()
std::string errmsg = "SOMETHING WENT TERRIBLY WRONG IN PARSING";
// TODO this pattern is 'working but broken'. need to use a datastructure for required members and iterate
// do NOT replace this with a switch case pattern
if ( loader_root.isMember("name") )
{ this->name = loader_root.get("name", errmsg).asString(); } else
throw UnitException("No name attribute specified when loading a unit.");
if ( loader_root.isMember("target") )
{ this->target = loader_root.get("target", errmsg).asString(); } else
throw UnitException("No target attribute specified when loading a unit.");
if ( loader_root.isMember("rectifier") )
{ this->rectifier = loader_root.get("rectifier", errmsg).asString(); } else
throw UnitException("No rectifier executable attribute specified when loading a unit.");
if ( loader_root.isMember("active") )
{ this->active = loader_root.get("active", errmsg).asBool(); } else
throw UnitException("No activation attribute specified when loading a unit.");
if ( loader_root.isMember("required") )
{ this->required = loader_root.get("required", errmsg).asBool(); } else
throw UnitException("No required attribute specified when loading a unit.");
if ( loader_root.isMember("log") )
{ this->stdout_log_flag = loader_root.get("log", errmsg).asBool(); } else
throw UnitException("No log attribute specified when loading a unit.");
if ( loader_root.isMember("rectify") )
{ this->rectify = loader_root.get("rectify", errmsg).asBool(); } else
throw UnitException("No rectify boolean attribute specified when loading a unit.");
// TODO functionize this
int uid = getuid();
struct passwd * upw;
std::string errmsg_user;
// if no user field is specified then default to the currently executing user
if ( ( upw = getpwuid(uid) ) == NULL )
{
throw UnitException( "Could not retrieve current user." );
} else {
errmsg_user = upw->pw_name;
}
// -TODO
if ( loader_root.isMember( "user" ) )
{ this->user = loader_root.get( "user", errmsg_user ).asString(); } else this->user = errmsg_user;
// TODO functionalize this
// get the current context gid as a backup value
int gid = getgid();
// declare the grp object to pull the name from once populated
struct group * grp;
// storage for backup value once retrieved
std::string errmsg_group;
// get the backup value and store it to errmsg_group
if ( ( grp = getgrgid( gid ) ) == NULL )
{
throw UnitException("Could not retrieve current group");
} else {
errmsg_group = grp->gr_name;
}
if ( loader_root.isMember( "group" ) )
{ this->group = loader_root.get( "group", errmsg_group ).asString(); } else this->group = grp->gr_name;
if ( loader_root.isMember( "shell" ) )
{ this->shell = loader_root.get( "shell", errmsg ).asString(); } else this->shell = "/usr/bin/env sh";
if ( loader_root.isMember( "environment") )
{ this->env_vars_file = loader_root.get( "environment", errmsg ).asString(); } else {
throw UnitException("No environment file specified for a unit, and environment files are required for unit definitions.");
}
this->populated = true;
return 0;
}
/// Unit::load_string - populates a Unit object from a supplies JSON-formatted string. It's stellar.
///
/// \param json_val - JSON-formatted string to populate from. See Unit::load_root() for details on required structure.
/// \return - The bool representation of success or failure.
int Unit::load_string(std::string json_val)
{
// serialize
this->load_json_string( json_val );
// deserialize
this->load_root( this->json_root );
return 0;
}
/// Unit::get_name - retrieves the name of the unit.
///
/// \return the name of the unit.
std::string Unit::get_name()
{
if ( ! this->populated ) { throw UnitException("Attempted to access an unpopulated unit."); }
return this->name;
}
/// Unit::get_target - retrieves the target of the unit.
///
/// \return the target of the unit.
std::string Unit::get_target()
{
if ( ! this->populated ) { throw UnitException("Attempted to access an unpopulated unit."); }
return this->target;
}
/// Unit::get_output - retrieves the output of the unit.
///
/// \return the output of the unit.
std::string Unit::get_output()
{
if ( ! this->populated ) { throw UnitException("Attempted to access an unpopulated unit."); }
return this->output;
}
/// Unit::get_rectifier - retrieves the rectifier of the unit.
///
/// \return the rectifier of the unit.
std::string Unit::get_rectifier()
{
if ( ! this->populated ) { throw UnitException("Attempted to access an unpopulated unit."); }
return this->rectifier;
}
/// Unit::get_active - retrieves the armed status of the unit.
///
/// \return the armed status of the unit.
bool Unit::get_active()
{
if ( ! this->populated ) { throw UnitException("Attempted to access an unpopulated unit."); }
return this->active;
}
/// Unit::get_required - retrieves the requirement status of the unit.
///
/// \return the requirement status of the unit.
bool Unit::get_required()
{
if ( ! this->populated ) { throw UnitException("Attempted to access an unpopulated unit."); }
return this->required;
}
/// Unit::get_rectify - retrieves the rectification status of the unit.
///
/// \return the rectification status of the unit.
bool Unit::get_rectify()
{
if ( ! this->populated ) { throw UnitException("Attempted to access an unpopulated unit."); }
return this->rectify;
}
/// Unit::get_user - retrieves the user context for the unit.
///
/// \return the string value of the user name.
std::string Unit::get_user()
{
if ( ! this->populated ) { throw UnitException("Attempted to access an unpopulated unit."); }
return this->user;
}
/// Unit::get_group - retrieves the group context for the unit.
///
/// \return the string value of the group name.
std::string Unit::get_group()
{
if ( ! this->populated ) { throw UnitException("Attempted to access an unpopulated unit."); }
return this->group;
}
/// Unit::get_shell - retrieves the shell path to use for the unit execution.
///
/// \return the string value of the shell path.
std::string Unit::get_shell()
{
if ( ! this->populated ) { throw UnitException("Attempted to access an unpopulated unit."); }
return this->shell;
}
/// Unit::get_env_vars_file - retrieves the file path to use for the unit environment file. This is a file that is
/// sourced by the chosen shell to populate any environment variables.
/// \return the string value of the shell path.
std::string Unit::get_env_vars_file()
{
if ( ! this->populated ) { throw UnitException("Attempted to access an unpopulated unit."); }
return this->env_vars_file;
}
/// Unit::get_stdout_log_flag() - retrieves the file path to use for the unit environment file. This is a file that is
/// sourced by the chosen shell to populate any environment variables.
/// \return the string value of the shell path.
bool Unit::get_stdout_log_flag()
{
if ( ! this->populated ) { throw UnitException("Attempted to access an unpopulated unit."); }
return this->stdout_log_flag;
}

View File

@ -1,108 +0,0 @@
/*
Rex - A configuration management and workflow automation tool that
compiles and runs in minimal environments.
© SILO GROUP and Chris Punches, 2020.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/* Unit.h
* Unit is a type that represents a safely deserialized JSON object which defines what actions are taken as rex
* iterates through it's Tasks in it's given Plan. They only define the behaviour on execution, while the tasks define
* which Units are executed and in what order (and which Units a given Task depends on.
*/
#ifndef FTEST_UNIT_H
#define FTEST_UNIT_H
#include <string>
#include "../../json/json.h"
#include "../low_level/JSON_Loader.h"
#include "../../Logger/Logger.h"
class Unit: JSON_Loader
{
private:
// the name of the test
std::string name;
// the path of the executable this test executes when run
std::string target;
// the desired output
// poll: would an empty value be good to indicate to rely solely on zero/non-zero exit code?
std::string output;
// the path of the executable this test runs when the target executable fails to produce output or a 0 exit code.
std::string rectifier;
// an indicator of whether the test is active or not
// this is used as a way to give definers a way to force executors to edit arbitrary fields or prevent
// execution of potentially dangerous or intrusive tests
bool active;
// an indicator of whether or not this test is required to pass.
// intended to be used as a flag to halt execution of further tests on failure
bool required;
// indicator of whether the rectifier executable should be run on test failures.
// if rectifier exits on non-zero return code, it should be trigger the behaviour indicated by required
bool rectify;
//indicator of whether stdout should log to file. used mainly to handle glitchy TUI systems when logs are being tailed.
bool stdout_log_flag;
// user to run process as.
// not intended for protected accounts, handle your own security
std::string user;
// group to run process as.
// not intended for protected accounts, handle your own security
std::string group;
// shell to use for env
std::string shell;
std::string env_vars_file;
public:
Unit( int LOG_LEVEL );
// loads a serialized jason::value object as a unit
int load_root( Json::Value loader_root );
// loads a string as a json string and deserializes that.
int load_string( std::string json_val );
// getters
std::string get_name();
std::string get_target();
std::string get_output();
std::string get_rectifier();
std::string get_env_vars_file();
bool get_active();
bool get_required();
bool get_rectify();
bool get_stdout_log_flag();
std::string get_user();
std::string get_group();
std::string get_shell();
private:
int LOG_LEVEL;
Logger slog;
};
#endif //FTEST_UNIT_H

View File

@ -1,22 +0,0 @@
/*
Rex - A configuration management and workflow automation tool that
compiles and runs in minimal environments.
© SILO GROUP and Chris Punches, 2020.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "loaders.h"

View File

@ -1,29 +0,0 @@
/*
Rex - A configuration management and workflow automation tool that
compiles and runs in minimal environments.
© SILO GROUP and Chris Punches, 2020.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef REX_LOADERS_H
#define REX_LOADERS_H
#include "../low_level/JSON_Loader.h"
#include "Suite.h"
#include "Plan.h"
#include "Conf.h"
#endif //REX_LOADERS_H

View File

@ -1,206 +0,0 @@
/*
Rex - A configuration management and workflow automation tool that
compiles and runs in minimal environments.
© SILO GROUP and Chris Punches, 2020.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "JSON_Loader.h"
/// JSON_Loader_NotReady - Exception thrown when a member function is called before data is populated.
class JSON_Loader_NotReady: public std::runtime_error { public:
JSON_Loader_NotReady(): std::runtime_error("JSON_Loader: Tried to access JSON without actually populating JSON.") {}
};
/// JSON_Loader_FileNotfound - Exception thrown when JSON_Loader can not find the file it is told to parse.
class JSON_Loader_FileNotFound: public std::runtime_error { public:
JSON_Loader_FileNotFound(): std::runtime_error("JSON_Loader: The requested file could not be found.") {}
};
/// JSON_Loader_InvalidJSON - Exception thrown when JSON_Loader fails to parse the JSON provided.
class JSON_Loader_InvalidJSON: public std::runtime_error { public:
JSON_Loader_InvalidJSON(): std::runtime_error("JSON_Loader: The JSON provided could not be parsed.") {}
};
/// JSON_Loader::JSON_Loader - Constructor for JSON_Loader base class. Simply inits to an unpopulated state.
///
/// The JSON_Loader type is a base type. It is meant to provide the functionalities shared between Suite and Plan.
JSON_Loader::JSON_Loader( int LOG_LEVEL ): slog( LOG_LEVEL, "_json_" )
{
this->populated = false;
this->LOG_LEVEL = LOG_LEVEL;
}
/// JSON_Loader::load_json_string - loads json from std::string into a json::value type and sets to protected member
/// 'json_root'.
///
/// \param input - The JSON-formatted string to serialize
/// \param verbose - Whether or not to print verbose information to STDOUT.
void JSON_Loader::load_json_string( std::string input )
{
// reads from a string into a Json::Value type.
Json::Reader json_reader;
// the deserialized json type to contain what's read by the reader
// Json::Value json_root;
// create the ifstream file handle
std::ifstream json_file_ifstream( input.c_str(), std::ifstream::binary );
// use the reader to parse the ifstream to the local property
bool parsingSuccessful = json_reader.parse( json_file_ifstream, this->json_root );
if (! parsingSuccessful )
{
this->slog.log( E_FATAL, "Failed to parse adhoc JSON value: " + json_reader.getFormattedErrorMessages() );
throw JSON_Loader_InvalidJSON();
} else {
this->slog.log( E_DEBUG, "Successfully parsed JSON string with " + std::to_string( this->json_root.size() ) + "elements. Value: '" + input + "'." );
}
// flag as ready for consumption
this->populated = true;
}
/// JSON_Loader::load_json_file - Loads JSON from a filepath into a serialized representation assigned as a local member
/// intended to be used as a buffer for further operations by base methods and derived class methods.
///
/// \param filename -
/// \param verbose
void JSON_Loader::load_json_file( std::string filename )
{
// reads from a file into a Json::Value type.
Json::Reader json_reader;
// the a deserialized json type to contain what's read by the reader
Json::Value json_root;
// first, check if the file exists
if (! exists( filename ) )
{
this->slog.log( E_FATAL, "File '" + filename + "' does not exist." );
throw JSON_Loader_FileNotFound();
}
// create the ifstream file handle
std::ifstream json_file_ifstream( filename, std::ifstream::binary );
// use the reader to parse the ifstream to the local property
bool parsingSuccessful = json_reader.parse( json_file_ifstream, this->json_root );
if (! parsingSuccessful )
{
this->slog.log( E_FATAL, "Failed to parse file '" + filename + "': " + json_reader.getFormattedErrorMessages() );
throw JSON_Loader_InvalidJSON();
} else {
// if in verbose mode, give the user an "it worked" message
this->slog.log( E_DEBUG, "Parsed '" + filename + "' with " + std::to_string( this->json_root.size() ) + " element(s)." );
}
// Flag as ready for consumption.
this->populated = true;
}
/// JSON_Loader::as_string - returns the string representation of json_root
std::string JSON_Loader::as_string()
{
if ( ! this->populated ) { throw JSON_Loader_NotReady(); }
return this->json_root.asString();
}
/// JSON_Loader::get_serialized - assigns the serialized representation of the value of a key (json::value)
///
/// \param input - A reference to the json::value object to receive the new value.
/// \param key - The JSON key name to assign the value to (the root of the json::value object by name)
/// \param verbose - Whether or not to print verbose output to STDOUT.
/// \return - Boolean indicator of success or failure (0|1)
int JSON_Loader::get_serialized( Json::Value &input, std::string key )
{
// throw if the class is not ready to be used.
if ( ! this->populated ) { throw JSON_Loader_NotReady(); }
if ( this->json_root.isMember( key ) )
{
// key was found so return it to the passed input ref
input = this->json_root[ key ];
return 0;
}
// key was not found
// verbose mode tells the user what key we were looking for.
this->slog.log( E_FATAL, "Failed to find key '" + key + "'." );
// exit code for failure
return 1;
}
/// JSON_Loader::get_string - assigns the serialized representation of the value of a key (json::value)
///
/// \param input - A reference to the json::value object to receive the new value.
/// \param key - The JSON key name to assign the value to (the root of the json::value object by name)
/// \param verbose - Whether or not to print verbose output to STDOUT.
/// \return - Boolean indicator of success or failure (0|1)
int JSON_Loader::get_string( std::string &input, std::string key )
{
// throw if the class is not ready to be used.
if ( ! this->populated ) { throw JSON_Loader_NotReady(); }
if ( this->json_root.isMember( key ) )
{
// key was found so return it to the passed input ref
input = this->json_root[ key ].asString();
return 0;
}
// key was not found
// verbose mode tells the user what key we were looking for.
this->slog.log( E_FATAL, "Failed to find key '" + key + "'." );
// exit code for failure
return 1;
}
/// JSON_Loader::get_bool - assigns the serialized representation of the value of a key (json::value)
///
/// \param input - A reference to the json::value object to receive the new value.
/// \param key - The JSON key name to assign the value to (the root of the json::value object by name)
/// \param verbose - Whether or not to print verbose output to STDOUT.
/// \return - Boolean indicator of success or failure (0|1)
int JSON_Loader::get_bool( bool & input, std::string key )
{
// throw if the class is not ready to be used.
if ( ! this->populated ) { throw JSON_Loader_NotReady(); }
if ( this->json_root.isMember( key ) )
{
// key was found so return it to the passed input ref
input = this->json_root[ key ].asBool();
return 0;
}
// key was not found
// verbose mode tells the user what key we were looking for.
this->slog.log( E_FATAL, "Failed to find key '" + key + "'." );
// exit code for failure
return 1;
}

View File

@ -1,61 +0,0 @@
/*
Rex - A configuration management and workflow automation tool that
compiles and runs in minimal environments.
© SILO GROUP and Chris Punches, 2020.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef REX_JLOADER_H
#define REX_JLOADER_H
#include "../../json/json.h"
#include <iostream>
#include <fstream>
#include <cstdlib>
#include <stdexcept>
#include "../misc/helpers.h"
#include "../../Logger/Logger.h"
class JSON_Loader
{
protected:
Json::Value json_root;
bool populated;
public:
// constructor
JSON_Loader( int LOG_LEVEL );
// load from json file
void load_json_file( std::string filename );
// load from std::string json
void load_json_string( std::string input );
// return as a JSONCPP serialized object
// deprecated -- these aren't really used.
// Json::Value as_serialized();
std::string as_string();
// safely handle deserialized type retrieval (if we want it to be safe)
int get_serialized( Json::Value & input, std::string key );
int get_string(std::string & input, std::string key );
int get_bool(bool & input, std::string key );
private:
Logger slog;
int LOG_LEVEL;
};
#endif //REX_JLOADER_H

View File

@ -1,72 +0,0 @@
/*
Rex - A configuration management and workflow automation tool that
compiles and runs in minimal environments.
© SILO GROUP and Chris Punches, 2020.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "helpers.h"
bool exists(const std::string& name)
{
struct stat buffer;
return (stat (name.c_str(), &buffer) == 0);
}
std::string get_working_path()
{
char temp[MAXPATHLEN];
return ( getcwd(temp, MAXPATHLEN) ? std::string( temp ) : std::string("") );
}
bool is_file( std::string path)
{
struct stat buf;
stat( path.c_str(), &buf );
return S_ISREG(buf.st_mode);
}
bool is_dir( std::string path )
{
struct stat buf;
stat( path.c_str(), &buf );
return S_ISDIR(buf.st_mode);
}
std::string get_8601()
{
auto now = std::chrono::system_clock::now();
auto itt = std::chrono::system_clock::to_time_t(now);
std::ostringstream ss;
// ss << std::put_time(gmtime(&itt), "%FT%TZ");
ss << std::put_time(localtime(&itt), "%Y-%m-%d_%H:%M:%S");
return ss.str();
}
void interpolate( std::string & text )
{
static std::regex env( "\\$\\{([^}]+)\\}" );
std::smatch match;
while ( std::regex_search( text, match, env ) )
{
const char * s = getenv( match[1].str().c_str() );
const std::string var( s == NULL ? "" : s );
text.replace( match[0].first, match[0].second, var );
}
}

View File

@ -51,3 +51,8 @@ void Logger::log( int LOG_LEVEL, std::string msg )
}
}
void Logger::log_task( int LOG_LEVEL, std::string task_name, std::string msg )
{
std::string final_msg = "[" + task_name + "] " + msg;
this->log( LOG_LEVEL, final_msg );
}

View File

@ -26,7 +26,7 @@
#include <iostream>
#include <iomanip>
#include <sstream>
#include "../loaders/misc/helpers.h"
#include "../misc/helpers.h"
enum L_LVL {
E_FATAL,
@ -36,13 +36,14 @@ enum L_LVL {
};
class Logger {
public:
Logger( int LOG_LEVEL, std::string mask );
void log( int LOG_LEVEL, std::string msg );
public:
Logger( int LOG_LEVEL, std::string mask );
void log( int LOG_LEVEL, std::string msg );
void log_task( int LOG_LEVEL, std::string task_name, std::string msg );
private:
int LOG_LEVEL;
std::string mask;
private:
int LOG_LEVEL;
std::string mask;
};

114
src/misc/helpers.cpp Normal file
View File

@ -0,0 +1,114 @@
#include "helpers.h"
/**
* @brief Determines if a file or directory exists
*
* This function takes a file or directory name as input and uses the `stat` function to determine if the
* specified file or directory exists.
*
* @param name The name of the file or directory to check
*
* @return `true` if the file or directory exists, `false` otherwise
*/
bool exists(const std::string& name)
{
struct stat buffer;
return (stat (name.c_str(), &buffer) == 0);
}
/**
* @brief Returns the current working directory as a string
*
* This function uses the `getcwd` function to obtain the current working directory. If the `getcwd`
* function fails, an empty string is returned.
*
* @return The current working directory as a string
*/
std::string get_working_path()
{
char temp[MAXPATHLEN];
if (!getcwd(temp, MAXPATHLEN)) {
return "";
}
return temp;
}
/**
* @brief Determines if a given file path is a regular file
*
* This function takes a file path as input and uses the `stat` function to obtain information about the
* file or directory. The `st_mode` member of the `stat` structure is then checked to determine if the
* file is a regular file or not.
*
* @param path The file path to be checked
*
* @return `true` if the file path is a regular file, `false` otherwise
*/
bool is_file( std::string path)
{
struct stat buf;
stat( path.c_str(), &buf );
return S_ISREG(buf.st_mode);
}
/**
* @brief Determines if a given file path is a directory
*
* This function takes a file path as input and uses the `stat` function to obtain information about the
* file or directory. The `st_mode` member of the `stat` structure is then checked to determine if the
* file is a directory or not.
*
* @param path The file path to be checked
*
* @return `true` if the file path is a directory, `false` otherwise
*/
bool is_dir( std::string path )
{
struct stat buf;
stat( path.c_str(), &buf );
return S_ISDIR(buf.st_mode);
}
/**
* @brief Returns a string representation of the current date and time in the format "YYYY-MM-DD_HH:MM:SS"
*
* This function uses the `std::chrono` library to obtain the current system time and convert it to a
* time_t value. It then uses the `strftime` function to format the date and time into a string with the
* format "YYYY-MM-DD_HH:MM:SS". The resulting string is returned as the function result.
*
* @return A string representation of the current date and time
*/
std::string get_8601()
{
auto now = std::chrono::system_clock::now();
auto itt = std::chrono::system_clock::to_time_t(now);
char buf[20];
strftime(buf, sizeof(buf), "%Y-%m-%d_%H:%M:%S", localtime(&itt));
return buf;
}
/**
* @brief Interpolates the environment variables in the input text
*
* This function takes a string reference as input and replaces all occurrences of
* environment variables in the format `${VAR_NAME}` with their corresponding values.
* If an environment variable is not set, it is replaced with an empty string.
*
* @param text The input text to be processed
*/
void interpolate( std::string & text )
{
static std::regex env( "\\$\\{([^}]+)\\}" );
std::smatch match;
while ( std::regex_search( text, match, env ) )
{
const char * s = getenv( match[1].str().c_str() );
const std::string var( s == NULL ? "" : s );
text.replace( match[0].first, match[0].second, var );
}
}

View File

@ -21,6 +21,7 @@
#ifndef REX_HELPERS_H
#define REX_HELPERS_H
#include <string>
#include <sys/stat.h>
#include <sys/param.h>
@ -50,4 +51,4 @@ std::string get_8601();
const char * command2args( std::string input_string );
#endif //REX_HELPERS_JH
#endif //REX_HELPERS_H

343
src/plan/Plan.cpp Normal file
View File

@ -0,0 +1,343 @@
/*
Rex - A configuration management and workflow automation tool that
compiles and runs in minimal environments.
© SILO GROUP and Chris Punches, 2020.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "Plan.h"
/**
* @class Plan_InvalidTaskIndex
* @brief Exception thrown when a Plan tries to access a contained Task's value by index not present in the Unit.
*
* This class is derived from std::runtime_error and is used to indicate that a Plan has tried to access a Task's value by index that is not present in the Unit.
* The constructor creates an error message with the description "Plan: Attempted to access a Task using an invalid index."
*/
class Plan_InvalidTaskIndex : public std::runtime_error {
public:
/**
* @brief Constructs a Plan_InvalidTaskIndex object with a default error message.
*
* The default error message is "Plan: Attempted to access a Task using an invalid index."
*/
Plan_InvalidTaskIndex() : std::runtime_error("Plan: Attempted to access a Task using an invalid index.") {}
};
/**
* @class Plan_InvalidTaskName
* @brief Exception thrown when a Plan tries to access a contained Task's value by name not present in the Unit.
*
* This class is derived from std::runtime_error and is used to indicate that a Plan has tried to access a Task's value by name that is not present in the Unit.
* The constructor creates an error message with the description "Plan: Attempted to access a Task using an invalid name."
*/
class Plan_InvalidTaskName : public std::runtime_error {
public:
/**
* @brief Constructs a Plan_InvalidTaskName object with a default error message.
*
* The default error message is "Plan: Attempted to access a Task using an invalid name."
*/
Plan_InvalidTaskName() : std::runtime_error("Plan: Attempted to access a Task using an invalid name.") {}
};
/**
* @class Plan_Task_GeneralExecutionException
* @brief Wrapper exception to catch exceptions thrown by the execution of Tasks.
*
* This class is derived from std::exception and is used as a wrapper to catch exceptions thrown by the execution of Tasks.
* It has two constructors, one that takes a C-style string error message and another that takes a C++ STL string error message.
* The error message is stored in the `msg_` member variable and can be accessed using the `what()` function.
*/
class Plan_Task_GeneralExecutionException : public std::exception {
public:
/**
* @brief Constructs a Plan_Task_GeneralExecutionException object with a C-style string error message.
*
* The error message is passed as a C-style string and is copied upon construction.
* The responsibility for deleting the char* lies with the caller.
*
* @param message C-style string error message.
*/
explicit Plan_Task_GeneralExecutionException(const char* message) : msg_(message) {}
/**
* @brief Constructs a Plan_Task_GeneralExecutionException object with a C++ STL string error message.
*
* The error message is passed as a C++ STL string.
*
* @param message The error message.
*/
explicit Plan_Task_GeneralExecutionException(const std::string& message) : msg_(message) {}
/**
* @brief Virtual destructor.
*
* The destructor is virtual to allow for subclassing.
*/
virtual ~Plan_Task_GeneralExecutionException() throw() {}
/**
* @brief Returns a pointer to the (constant) error description.
*
* Returns a pointer to the constant error description stored in the `msg_` member variable.
* The underlying memory is in possession of the Exception object. Callers must not attempt to free the memory.
*
* @return A pointer to a const char*.
*/
virtual const char* what() const throw() { return msg_.c_str(); }
protected:
/**
* @brief Error message.
*/
std::string msg_;
};
/**
* @class Plan_Task_Missing_Dependency
* @brief Exception thrown when a Task is missing a required dependency.
*
* This class is derived from std::exception and is used to indicate that a Task is missing a required dependency.
* It has two constructors, one that takes a C-style string error message and another that takes a C++ STL string error message.
* The error message is stored in the `msg_` member variable and can be accessed using the `what()` function.
*/
class Plan_Task_Missing_Dependency : public std::exception {
public:
/**
* @brief Constructs a Plan_Task_Missing_Dependency object with a C-style string error message.
*
* The error message is passed as a C-style string and is copied upon construction.
* The responsibility for deleting the char* lies with the caller.
*
* @param message C-style string error message.
*/
explicit Plan_Task_Missing_Dependency(const char* message) : msg_(message) {}
/**
* @brief Constructs a Plan_Task_Missing_Dependency object with a C++ STL string error message.
*
* The error message is passed as a C++ STL string.
*
* @param message The error message.
*/
explicit Plan_Task_Missing_Dependency(const std::string& message) : msg_(message) {}
/**
* @brief Virtual destructor.
*
* The destructor is virtual to allow for subclassing.
*/
virtual ~Plan_Task_Missing_Dependency() throw() {}
/**
* @brief Returns a pointer to the (constant) error description.
*
* Returns a pointer to the constant error description stored in the `msg_` member variable.
* The underlying memory is in possession of the Exception object. Callers must not attempt to free the memory.
*
* @return A pointer to a const char*.
*/
virtual const char* what() const throw() { return msg_.c_str(); }
protected:
/**
* @brief Error message.
*/
std::string msg_;
};
/**
* @brief Constructor for Plan class.
*
* A Plan is a managed container for a Task vector. These tasks reference Units that are defined in the Units files (Suite).
* If Units are definitions, Tasks are selections of those definitions to execute, and if Units together form a Suite,
* Tasks together form a Plan.
*
* @param configuration A pointer to a Conf object that holds the configuration information.
* @param LOG_LEVEL The logging level for the plan.
*/
Plan::Plan(Conf * configuration, int LOG_LEVEL ): JSON_Loader(LOG_LEVEL ), slog(LOG_LEVEL, "_plan_" )
{
this->configuration = configuration;
this->LOG_LEVEL = LOG_LEVEL;
}
/**
* @brief Load the plan file and append intact Units as they're deserialized from the provided file.
*
* @param filename The filename to load the plan from.
*/
void Plan::load_plan_file( std::string filename )
{
// plan always loads from file
this->load_json_file( filename );
// staging buffer
Json::Value jbuff;
// fill the jbuff staging buffer with a json::value object in the supplied filename
if ( this->get_serialized( jbuff, "plan" ) == 0 )
{
this->json_root = jbuff;
}
// iterate through the json::value members that have been loaded. append to this->tasks vector
// buffer for tasks to append:
Task tmp_T = Task( this->LOG_LEVEL );
for ( int index = 0; index < this->json_root.size(); index++ )
{
tmp_T.load_root( this->json_root[ index ] );
this->tasks.push_back( tmp_T );
this->slog.log( LOG_INFO, "Added task \"" + tmp_T.get_name() + "\" to Plan." );
}
}
/**
* @brief Retrieve a task by index.
*
* @param result The variable receiving the value.
* @param index The numerical index in the Task vector to retrieve a value for.
*
* @throws Plan_InvalidTaskIndex if the index is greater than the size of the Task vector.
*/
void Plan::get_task(Task & result, int index )
{
if ( index <= this->tasks.size() )
{
result = this->tasks[ index ];
} else {
throw Plan_InvalidTaskIndex();
}
}
/**
* @brief Load the units corresponding to each task in the plan from the given Suite.
*
* @param unit_definitions The Suite to load definitions from.
*/
void Plan::load_definitions( Suite unit_definitions )
{
// placeholder Unit
Unit tmp_U = Unit( this->LOG_LEVEL );
// for every task in the plan:
for (int i = 0; i < this->tasks.size(); i++ )
{
// load the tmp_U corresponding to that task name
unit_definitions.get_unit( tmp_U, this->tasks[i].get_name() );
// then have that task attach a copy of tmp_U
this->tasks[i].load_definition( tmp_U );
}
}
/**
* @brief Retrieve a task by name.
*
* @param result The variable receiving the value.
* @param provided_name The name to find a task by.
*
* @throws Plan_InvalidTaskName if a task with the provided name is not found.
*/
void Plan::get_task(Task & result, std::string provided_name )
{
bool foundMatch = false;
for ( int i = 0; i < this->tasks.size(); i++ )
{
if ( this->tasks[i].get_name() == provided_name )
{
result = this->tasks[i];
foundMatch = true;
break;
}
}
if (! foundMatch )
{
this->slog.log( E_FATAL, "Task name \"" + provided_name + "\" was referenced but not defined!" );
throw Plan_InvalidTaskName();
}
}
/**
* @brief Check whether all dependencies for a task with the given name are complete.
*
* @param name The name of the task in the plan to check met dependencies for.
*
* @return Boolean representation of whether all dependencies are complete or not.
*/
bool Plan::all_dependencies_complete(std::string name)
{
// get the task by name
Task named_task = Task( this->LOG_LEVEL );
this->get_task( named_task, name );
// get the dependencies of that task
std::vector<std::string> deps = named_task.get_dependencies();
// create an empty task to assign values to during iteration
Task tmpTask = Task( this->LOG_LEVEL );
// iterate through its dependencies
for ( int i = 0; i < deps.size(); i++ )
{
this->get_task( tmpTask, deps[i]);
if (! tmpTask.is_complete() )
{
// error message?
return false;
}
}
return true;
}
/**
* @brief Iterate through all tasks in the plan and execute them.
*/
void Plan::execute()
{
// for each task in this plan
for ( int i = 0; i < this->tasks.size(); i++ )
{
if (this->all_dependencies_complete(this->tasks[i].get_name()) )
{
this->slog.log( E_INFO, "[ '" + this->tasks[i].get_name() + "' ] Executing..." );
try {
this->tasks[i].execute( this->configuration );
}
catch (std::exception& e) {
this->slog.log( E_FATAL, "[ '" + this->tasks[i].get_name() + "' ] Report: " + e.what() );
throw Plan_Task_GeneralExecutionException("Could not execute task.");
}
} else {
// not all deps met for this task
this->slog.log( E_FATAL, "[ '" + this->tasks[i].get_name() + "' ] This task was specified in the Plan but not executed due to missing dependencies. Please revise your plan." );
throw Plan_Task_Missing_Dependency( "Unmet dependency for task." );
}
}
}

105
src/plan/Plan.h Normal file
View File

@ -0,0 +1,105 @@
/*
Rex - A configuration management and workflow automation tool that
compiles and runs in minimal environments.
© SILO GROUP and Chris Punches, 2020.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef REX_PLAN_H
#define REX_PLAN_H
#include "../json_support/JSON.h"
#include "../logger/Logger.h"
#include "../config/Config.h"
#include "Task.h"
#include <string>
/**
* @class Plan
* @brief A managed container for a Task vector.
*
* A Plan is a managed container for a Task vector. These tasks reference Units that are defined in the Units files (Suite).
* If Units are definitions, Tasks are selections of those definitions to execute, and if Units together form a Suite, Tasks
* together form a Plan.
*/
class Plan: public JSON_Loader
{
private:
// storage for the tasks that make up the plan
std::vector<Task> tasks;
Conf * configuration;
int LOG_LEVEL;
Logger slog;
public:
/**
* @brief Constructor for Plan class.
*
* @param configuration Configuration object.
* @param LOG_LEVEL Logging level.
*/
Plan(Conf * configuration, int LOG_LEVEL );
/**
* @brief Load the plan from a file and append tasks to the task vector.
*
* @param filename The filename to load the plan from.
*/
void load_plan_file( std::string filename );
/**
* @brief Retrieve a task by name.
*
* @param result The variable receiving the value.
* @param provided_name The name to find a task by.
*
* @throws Plan_InvalidTaskName if a task with the provided name is not found.
*/
void get_task( Task & result, std::string provided_name );
/**
* @brief Retrieve a task by index.
*
* @param result The variable receiving the value.
* @param index The numerical index in the Task vector to retrieve a value for.
*
* @throws Plan_InvalidTaskIndex if the index is greater than the size of the Task vector.
*/
void get_task( Task & result, int index );
/**
* @brief Load the units corresponding to each task in the plan from the given Suite.
*
* @param unit_definitions The Suite to load definitions from.
*/
void load_definitions( Suite unit_definitions );
/**
* @brief Check whether all dependencies for a task with the given name are complete.
*
* @param name The name of the task in the plan to check met dependencies for.
*
* @return Boolean representation of whether all dependencies are complete or not.
*/
bool all_dependencies_complete(std::string name);
/**
* @brief Iterate through all tasks in the plan and execute them.
*/
void execute();
};
#endif //REX_PLAN_H

621
src/plan/Task.cpp Normal file
View File

@ -0,0 +1,621 @@
#include "Task.h"
/*
Rex - A configuration management and workflow automation tool that
compiles and runs in minimal environments.
© SILO GROUP and Chris Punches, 2020.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "Task.h"
/**
* @class Task_InvalidDataStructure
* @brief Exception thrown when a Task is defined with invalid JSON.
*
* This exception is derived from the standard runtime_error exception and is thrown
* when a Task object is defined with an invalid JSON structure.
*/
class Task_InvalidDataStructure: public std::runtime_error {
public:
/**
* @brief Constructs a Task_InvalidDataStructure object.
*
* This constructor creates a Task_InvalidDataStructure object with an error message
* indicating that a task was attempted to be accessed with an invalid data structure.
*/
Task_InvalidDataStructure(): std::runtime_error("Task: Attempted to access a member of a Task that is not set.") {}
};
/**
* @class Task_NotReady
* @brief Exception thrown when a Task is not ready to be executed.
*
* This exception is derived from the standard runtime_error exception and is thrown
* when a Task object is not well defined and cannot be executed.
*/
class Task_NotReady: public std::runtime_error {
public:
/**
* @brief Constructs a Task_NotReady object.
*
* This constructor creates a Task_NotReady object with an error message
* indicating that a task was attempted to be executed but its Unit was not well defined.
*/
Task_NotReady(): std::runtime_error("Task: Attempted to execute a Task whose Unit is not well defined.") {}
};
/**
* @class TaskException
* @brief Exception thrown for errors related to Tasks.
*
* This exception is a base class for exceptions related to Tasks and is derived
* from the standard exception class.
*/
class TaskException: public std::exception
{
public:
/**
* @brief Constructs a TaskException object with a C-style string error message.
*
* This constructor creates a TaskException object with a C-style string error message.
* The string contents are copied upon construction and the responsibility for deleting
* the char* lies with the caller.
*
* @param message C-style string error message.
*/
explicit TaskException(const char* message): msg_(message) {}
/**
* @brief Constructs a TaskException object with a C++ STL string error message.
*
* This constructor creates a TaskException object with a C++ STL string error message.
*
* @param message The error message.
*/
explicit TaskException(const std::string& message): msg_(message) {}
/**
* @brief Virtual destructor.
*
* This destructor is virtual to allow for subclassing.
*/
virtual ~TaskException() throw () {}
/**
* @brief Returns a pointer to the error description.
*
* This function returns a pointer to the error description.
* The underlying memory is in posession of the Exception object and callers must not attempt
* to free the memory.
*
* @return A pointer to a const char*.
*/
virtual const char* what() const throw () { return msg_.c_str(); }
protected:
/**
* @brief Error message.
*/
std::string msg_;
};
/**
* @class Task
* @brief The building block of a Plan indicating of which Unit to execute and its dependencies.
*
* The Task class represents a single unit of work in a Plan. It specifies which Unit should be executed
* and any dependencies that must be satisfied before it can be executed.
*/
/**
* @brief Constructor for the Task class.
*
* This constructor initializes a Task object with a specified log level and creates log and definition objects.
* The task is set as not complete and not defined by default.
*
* @param LOG_LEVEL The log level for this Task object.
*/
Task::Task( int LOG_LEVEL ):
slog( LOG_LEVEL, "_task_" ),
definition( LOG_LEVEL )
{
// it hasn't executed yet.
this->complete = false;
// it hasn't been matched with a definition yet.
this->defined = false;
this->LOG_LEVEL = LOG_LEVEL;
}
/**
* @brief Loads JSON values into private members.
*
* This function loads JSON values into the private members of a Task object.
*
* @param loader_root The Json::Value to populate from.
* @throws Task_InvalidDataStructure if the "name" member is not present in the JSON object.
*/
void Task::load_root(Json::Value loader_root )
{
if ( loader_root.isMember("name") ) {
this->name = loader_root.get("name", "?").asString();
}
else {
throw Task_InvalidDataStructure();
}
// fetch as Json::Value array obj
Json::Value des_dep_root = loader_root.get("dependencies", 0);
// iterate through each member of that obj
for ( int i = 0; i < des_dep_root.size(); i++ ) {
// add each string to dependencies
if ( des_dep_root[i].asString() != "" )
{
this->dependencies.push_back( des_dep_root[i].asString() );
this->slog.log( E_INFO, "Added dependency \"" + des_dep_root[i].asString() + "\" to task \"" + this->get_name() + "\"." );
}
}
}
/**
* @brief Retrieves the name of the current Task.
*
* @return The name of the Task as a string.
*/
std::string Task::get_name()
{
return this->name;
}
/**
* @brief Loads a unit to a local member. Used to tie Units to Tasks.
*
* @param selected_unit The unit to attach.
* @param verbose Whether to print to STDOUT.
*/
void Task::load_definition( Unit selected_unit )
{
this->definition = selected_unit;
this->slog.log( E_INFO, "Loaded definition \"" + selected_unit.get_name() + "\" as task in configured plan.");
this->defined = true;
}
/**
* @brief Indicates if the task executed successfully.
*
* @return True if the task completed successfully, false otherwise.
*/
bool Task::is_complete()
{
return this->complete;
}
/**
* @brief Marks the task as complete.
*/
void Task::mark_complete()
{
this->complete = true;
}
/**
* @brief Returns a pointer to the dependencies vector.
*
* @return A pointer to the vector containing the task's dependencies.
*/
std::vector<std::string> Task::get_dependencies()
{
return this->dependencies;
}
/**
* @brief Indicates if the task has attached its definition from a Suite.
*
* @return True if the task has a definition, false otherwise.
*/
bool Task::has_definition()
{
return this->defined;
}
bool is_abs_path( const std::string &path )
{
if ( path[0] == '/' )
{
return true;
}
return false;
}
bool abspathExists(const std::string &path) {
if ( is_abs_path( path ) )
{
struct stat buffer;
return (stat(path.c_str(), &buffer) == 0);
} else {
return false;
}
}
std::string path_from_str( const std::string &inputString )
{
std::string result = "";
// Find the position of the first space character in the input string
size_t spacePosition = inputString.find(' ');
// If there is a space in the input string, extract the substring up to the space
if (spacePosition != std::string::npos) {
result = inputString.substr(0, spacePosition);
}
// Otherwise, just return the entire input string
else {
result = inputString;
}
return result;
}
bool createDirectory(const std::string& path) {
// Check if the directory already exists
struct stat info;
if (stat(path.c_str(), &info) == 0 && S_ISDIR(info.st_mode)) {
return true; // Directory already exists
}
// Create the directory recursively
size_t pos = 0;
std::string dir;
while ((pos = path.find_first_of('/', pos + 1)) != std::string::npos) {
dir = path.substr(0, pos);
if (stat(dir.c_str(), &info) != 0) { // Directory does not exist
if (mkdir(dir.c_str(), S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH) != 0) {
return false; // Failed to create directory
}
} else if (!S_ISDIR(info.st_mode)) {
return false; // Path segment exists but is not a directory
}
}
// Create the final directory
if (mkdir(path.c_str(), S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH) != 0) {
return false; // Failed to create directory
}
return true;
}
bool Task::prepare_logs( std::string task_name, std::string logs_root, std::string timestamp )
{
std::string full_path = logs_root + "/" + task_name;
bool ret = false;
ret = createDirectory( full_path );
if (ret)
{
this->slog.log_task( E_INFO, "LOG_CREATE", "Logging will be at '" + full_path + "'." );
} else {
this->slog.log_task( E_FATAL, "LOG_CREATE", "Creation of directory path '" + full_path + "' failed." );
}
return ret;
}
/// Task::execute - execute a task's unit definition.
/// See the design document for what flow control needs to look like here.
/// \param verbose - Verbosity level - not implemented yet.
void Task::execute( Conf * configuration )
{
if ( ! this->has_definition() )
{
throw Task_NotReady();
}
bool override_working_dir = this->definition.get_set_working_directory();
bool is_shell_command = this->definition.get_is_shell_command();
bool supply_environment = this->definition.get_supply_environment();
bool rectify = this->definition.get_rectify();
bool active = this->definition.get_active();
bool required = this->definition.get_required();
bool set_user_context = this->definition.get_set_user_context();
bool force_pty = this->definition.get_force_pty();
std::string task_name = this->definition.get_name();
std::string command = this->definition.get_target();
std::string shell_name = this->definition.get_shell_definition();
Shell shell_definition = configuration->get_shell_by_name( shell_name );
std::string new_working_dir = this->definition.get_working_directory();
std::string rectifier = this->definition.get_rectifier();
std::string user = this->definition.get_user();
std::string group = this->definition.get_group();
std::string environment_file = this->definition.get_environment_file();
std::string logs_root = configuration->get_logs_path();
this->slog.log_task( E_DEBUG, task_name, "Using unit definition: \"" + task_name + "\"." );
// sanitize all path inputs from unit definition to be either absolute paths or relative to
// project_root
if ( supply_environment )
{
if (! is_shell_command )
{
throw TaskException("Garbage input: Supplied a shell environment file for a non-shell target.");
}
}
if ( ! active )
{
throw TaskException("Somehow tried to execute a task with an inactive unit definition.");
}
if (! is_abs_path( path_from_str( command ) ) )
{
command = configuration->get_project_root() + "/" + command;
}
if ( rectify )
{
if (! is_abs_path( path_from_str( rectifier ) ) )
{
rectifier = configuration->get_project_root() + "/" + rectifier;
}
}
if ( supply_environment )
{
if (! is_abs_path( environment_file ) )
{
environment_file = configuration->get_project_root() + "/" + environment_file;
}
}
if ( override_working_dir )
{
if (! is_abs_path( new_working_dir ) )
{
new_working_dir = configuration->get_project_root() + "/" + new_working_dir;
}
}
if (! is_abs_path( configuration->get_logs_path() ) )
{
logs_root = configuration->get_project_root() + "/" + logs_root;
}
std::string timestamp = get_8601();
// set these first so the pre-execution logs get there.
/*
* create the logs dir here
*/
if (! this->prepare_logs( task_name, logs_root, timestamp ) )
{
throw TaskException("Could not prepare logs for task execution at '" + logs_root + "'.");
}
std::string stdout_log_file = logs_root + "/" + timestamp + ".stdout.log";
std::string stderr_log_file = logs_root + "/" + timestamp + ".stderr.log";
// check if working directory is to be set
if ( override_working_dir )
{
// if so, set the CWD.
chdir( new_working_dir.c_str() );
this->slog.log_task( E_INFO, task_name, "Setting working directory: " + new_working_dir );
}
if ( is_shell_command )
{
this->slog.log_task( E_INFO, task_name, "Vars file: " + environment_file );
this->slog.log_task( E_INFO, task_name, "Shell: " + shell_definition.path );
}
// a[0] execute target
// TODO ...sourcing on the shell for variables and environment population doesn't have a good smell.
// it does prevent unexpected behaviour from reimplementing what bash does though
this->slog.log_task( E_INFO, task_name, "Executing target: \"" + command + "\"." );
int return_code = lcpex(
command,
stdout_log_file,
stderr_log_file,
set_user_context,
user,
group,
force_pty,
is_shell_command,
shell_definition.path,
shell_definition.execution_arg,
supply_environment,
shell_definition.source_cmd,
environment_file
);
// **********************************************
// d[0] Error Code Check
// **********************************************
if ( return_code == 0 )
{
// d[0].0 ZERO
this->slog.log_task( E_INFO, task_name, "Target succeeded. Marking as complete." );
this->mark_complete();
// a[1] NEXT
return;
}
if ( return_code != 0 )
{
// d[0].1 NON-ZERO
this->slog.log_task( E_WARN, task_name, "Target failed with exit code " + std::to_string( return_code ) + "." );
// **********************************************
// d[1] Rectify Check
// **********************************************
if (! this->definition.get_rectify() )
{
// d[1].0 FALSE
// **********************************************
// d[2] Required Check
// **********************************************
if (! required )
{
// d[2].0 FALSE
// a[2] NEXT
this->slog.log_task( E_INFO, task_name, "This task is not required to continue the plan. Moving on." );
return;
} else {
// d[2].1 TRUE
// a[3] EXCEPTION
this->slog.log_task( E_FATAL, task_name, "Task is required, and failed, and rectification is not enabled." );
throw TaskException( "Task failed: " + task_name );
}
// **********************************************
// end - d[2] Required Check
// **********************************************
}
if ( rectify )
{
// d[1].1 TRUE (Rectify Check)
this->slog.log_task( E_INFO, task_name, "Rectification pattern is enabled." );
// a[4] Execute RECTIFIER
this->slog.log_task( E_INFO, task_name, "Executing rectification: " + rectifier + "." );
int rectifier_error = lcpex(
rectifier,
stdout_log_file,
stderr_log_file,
set_user_context,
user,
group,
force_pty,
is_shell_command,
shell_definition.path,
shell_definition.execution_arg,
supply_environment,
shell_definition.source_cmd,
environment_file
);
// **********************************************
// d[3] Error Code Check for Rectifier
// **********************************************
if ( rectifier_error != 0 )
{
// d[3].1 Non-Zero
this->slog.log_task( E_WARN, task_name, "Rectification failed with exit code " + std::to_string( rectifier_error ) + "." );
// **********************************************
// d[4] Required Check
// **********************************************
if ( ! required ) {
// d[4].0 FALSE
// a[5] NEXT
this->slog.log_task( E_INFO, task_name, "This task is not required to continue the plan. Moving on." );
return;
} else {
// d[4].1 TRUE
// a[6] EXCEPTION
this->slog.log_task( E_FATAL, task_name, "Task is required, but failed, and rectification failed. Lost cause." );
throw TaskException( "Lost cause, task failure." );
}
// **********************************************
// end - d[4] Required Check
// **********************************************
}
// d[3] check exit code of rectifier
if ( rectifier_error == 0 )
{
// d[3].0 Zero
this->slog.log_task( E_INFO, task_name, "Rectification returned successfully." );
// a[7] Re-execute Target
this->slog.log_task( E_INFO, task_name, "Re-Executing target '" + command + "'." );
int retry_code = lcpex(
command,
stdout_log_file,
stderr_log_file,
set_user_context,
user,
group,
force_pty,
is_shell_command,
shell_definition.path,
shell_definition.execution_arg,
supply_environment,
shell_definition.source_cmd,
environment_file
);
// **********************************************
// d[5] Error Code Check
// **********************************************
if ( retry_code == 0 )
{
// d[5].0 ZERO
// a[8] NEXT
this->slog.log_task( E_INFO, task_name, "Re-execution was successful." );
return;
} else {
// d[5].1 NON-ZERO
this->slog.log_task( E_WARN, task_name, "Re-execution failed with exit code " + std::to_string( retry_code ) + "." );
// **********************************************
// d[6] Required Check
// **********************************************
if ( ! required )
{
// d[6].0 FALSE
// a[9] NEXT
this->slog.log_task( E_INFO, task_name, "This task is not required to continue the plan. Moving on." );
return;
} else {
// d[6].1 TRUE
// a[10] EXCEPTION
this->slog.log_task( E_FATAL, task_name, "Task is required, and failed, then rectified but rectifier did not heal the condition causing the target to fail. Cannot proceed with Plan." );
throw TaskException( "Lost cause, task failure." );
}
// **********************************************
// end - d[6] Required Check
// **********************************************
}
}
}
// **********************************************
// end d[1] Rectify Check
// **********************************************
}
}

View File

@ -1,39 +1,35 @@
/*
Rex - A configuration management and workflow automation tool that
compiles and runs in minimal environments.
© SILO GROUP and Chris Punches, 2020.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef REX_TASK_H
#define REX_TASK_H
#include "../json_support/JSON.h"
#include "../suite/Unit.h"
#include "../suite/Suite.h"
#include "../config/Config.h"
#include "../misc/helpers.h"
#include "../lcpex/liblcpex.h"
#include <string>
#include <unistd.h>
#include "../../json/json.h"
#include "Unit.h"
#include "Suite.h"
#include "Conf.h"
#include <stdio.h>
#include "../../Sproc/Sproc.h"
#include "../misc/helpers.h"
#include <sys/stat.h>
class Task
{
protected:
// the name of this task
std::string name;
@ -51,7 +47,10 @@ class Task
// the readiness of this task to execute
bool defined;
public:
bool prepare_logs( std::string task_name, std::string logs_root, std::string timestamp );
public:
// constructor
Task( int LOG_LEVEL );
@ -68,16 +67,17 @@ class Task
std::string get_name();
// execute this task's definition
void execute(Conf * configuration );
void execute( Conf * configuration );
void mark_complete();
// returns a pointer to the dependencies vector
std::vector<std::string> get_dependencies();
private:
Logger slog;
int LOG_LEVEL;
private:
Logger slog;
int LOG_LEVEL;
};
#endif //REX_TASK_H
#endif //REX_TASK_H

64
src/shells/shells.cpp Normal file
View File

@ -0,0 +1,64 @@
#include "shells.h"
class ShellException: public std::exception
{
public:
explicit ShellException(const char* message):
msg_(message)
{}
explicit ShellException(const std::string& message):
msg_(message)
{}
virtual ~ShellException() throw (){}
virtual const char* what() const throw (){
return msg_.c_str();
}
protected:
std::string msg_;
};
Shell::Shell(int LOG_LEVEL): JSON_Loader( LOG_LEVEL ), slog( LOG_LEVEL, "_shell" )
{
this->LOG_LEVEL = LOG_LEVEL;
}
int Shell::load_root( Json::Value loader_root )
{
// TODO this needs reworked to have errmsg actually end up as a null return from json::value.get()
std::string errmsg = "SOMETHING WENT TERRIBLY WRONG IN PARSING";
if ( loader_root.isMember("name") )
{
this->name = loader_root.get( "name", errmsg ).asString();
} else {
throw ShellException("No name attribute specified when loading a shell definition.");
}
if ( loader_root.isMember("path") )
{
this->path = loader_root.get( "path", errmsg ).asString();
} else {
throw ShellException("No path attribute specified when loading a shell definition.");
}
if ( loader_root.isMember("execution_arg") )
{
this->execution_arg = loader_root.get( "execution_arg", errmsg ).asString();
} else {
throw ShellException("No execution_arg attribute specified when loading a shell definition.");
}
if ( loader_root.isMember("execution_arg") )
{
this->execution_arg = loader_root.get( "execution_arg", errmsg ).asString();
} else {
throw ShellException("No execution_arg attribute specified when loading a shell definition.");
}
return 0;
}

26
src/shells/shells.h Normal file
View File

@ -0,0 +1,26 @@
#ifndef REX_SHELLS_H
#define REX_SHELLS_H
#include "../json_support/JSON.h"
#include <string>
class Shell: public JSON_Loader {
public:
Shell( int LOG_LEVEL );
int load_root( Json::Value loader_root );
std::string name;
std::string path;
std::string execution_arg;
std::string source_cmd;
private:
std::string shell_definitions_path;
int LOG_LEVEL;
Logger slog;
};
#endif //REX_SHELLS_H

View File

@ -1,93 +1,106 @@
/*
Rex - A configuration management and workflow automation tool that
compiles and runs in minimal environments.
© SILO GROUP and Chris Punches, 2020.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include <string.h>
#include <syslog.h>
#include <sys/stat.h>
#include "Suite.h"
#include <dirent.h>
/// Suite_InvalidUnitMember - Exception thrown when a Suite tries to access a contained Unit's value that is not
/// present in the Unit.
#include "Suite.h"
/**
* @class SuiteException
* @brief Exception thrown when a Suite tries to access a contained Unit's value that is not present in the Unit.
*
* This exception is thrown when a Suite object tries to access a value in a contained Unit object that is not present in the Unit. The error message is stored in the `msg_` member variable and can be retrieved with the `what` method.
*/
class SuiteException: public std::exception
{
public:
/** Constructor (C strings).
* @param message C-style string error message.
* The string contents are copied upon construction.
* Hence, responsibility for deleting the char* lies
* with the caller.
*/
explicit SuiteException(const char* message):
msg_(message)
{
}
public:
/**
* @brief Constructor for SuiteException class (C-style string).
*
* @param message C-style string error message. The string contents are copied upon construction. The caller is responsible for deleting the char*.
*/
explicit SuiteException(const char* message):
msg_(message)
{
}
/** Constructor (C++ STL strings).
* @param message The error message.
*/
explicit SuiteException(const std::string& message):
msg_(message)
{}
/**
* @brief Constructor for SuiteException class (C++ STL string).
*
* @param message The error message.
*/
explicit SuiteException(const std::string& message):
msg_(message)
{}
/** Destructor.
* Virtual to allow for subclassing.
*/
virtual ~SuiteException() throw (){}
/**
* @brief Destructor for SuiteException class.
*
* Virtual to allow for subclassing.
*/
virtual ~SuiteException() throw (){}
/** Returns a pointer to the (constant) error description.
* @return A pointer to a const char*. The underlying memory
* is in posession of the Exception object. Callers must
* not attempt to free the memory.
*/
virtual const char* what() const throw (){
return msg_.c_str();
}
/**
* @brief Get the error message.
*
* @return A pointer to a const char*. The underlying memory is in the possession of the Exception object. Callers should not attempt to free the memory.
*/
virtual const char* what() const throw (){
return msg_.c_str();
}
protected:
/** Error message.
*/
std::string msg_;
protected:
/// Error message.
std::string msg_;
};
/// Suite::Suite() - Constructor for Suite class. The Suite class is simply a managed container for a Unit vector.
/// Once instantiated, all methods will require either a JSON file or string to be loaded as deserialized Unit types
/// before being called or will simply throw an exception.
///
/// From the high level, a Suite contains the full definitions of all potential Units to execute defined in the Unit
/// definition files that it is loading. It is meant to be used in such a way that as the application iterates through
/// the Task objects contained by the application Plan, it will iterate through the appplication's Suite, which contains
/// the definition of all available Tasks. In this manner, defining units and executing units are split into separate
/// human processes to allow modularly developed profiles of test suites. As inferred, Unit is expected to be one of
/// the two types that are only instantiated once per application run, though it is designed to be used more than once
/// if the implementor so desires.
Suite::Suite( int LOG_LEVEL ): JSON_Loader( LOG_LEVEL ), slog( LOG_LEVEL, "_suite" )
/**
* @brief Constructor for Suite class.
*
* The Suite class is simply a managed container for a Unit vector.
* Once instantiated, all methods will require either a JSON file or string to be loaded as deserialized Unit types
* before being called or will simply throw an exception.
* From the high level, a Suite contains the full definitions of all potential Units to execute defined in the Unit
* definition files that it is loading. It is meant to be used in such a way that as the application iterates through
* the Task objects contained by the application Plan, it will iterate through the appplication's Suite, which contains
* the definition of all available Tasks. In this manner, defining units and executing units are split into separate
* human processes to allow modularly developed profiles of test suites. As inferred, Unit is expected to be one of
* the two types that are only instantiated once per application run, though it is designed to be used more than once
* if the implementor so desires.
*/
Suite::Suite( int LOG_LEVEL ): JSON_Loader( LOG_LEVEL ), slog( LOG_LEVEL, "_suite" )
{
this->LOG_LEVEL;
}
/**
* @brief Get the list of unit files in a directory
*
* This function retrieves all the files with a ".units" extension in the specified directory.
*
* @param[out] files The list of unit files
* @param[in] path The path of the directory to search
*
* @return None
*/
void Suite::get_units_from_dir( std::vector<std::string> * files, std::string path )
{
// TODO: this whole method probably needs rewritten
DIR* dirFile = opendir( path.c_str() );
if ( dirFile )
{
@ -127,11 +140,16 @@ void Suite::get_units_from_dir( std::vector<std::string> * files, std::string pa
}
/// Suite::load_units_file - Uses the json_root buffer on each run to append intact Units as they're
/// deserialized from the provided file.
///
/// \param units_path - The file to pull the JSON-formatted units from.
/// \param verbose - Whether to print verbose output to STDOUT.
/**
* @brief Load units from a file or directory
*
* This function loads units from a file or directory containing unit files.
* The unit files must be in JSON format.
*
* @param[in] units_path The path to the file or directory containing unit files
*
* @return None
*/
void Suite::load_units_file( std::string units_path )
{
std::vector<std::string> unit_files;
@ -181,10 +199,14 @@ void Suite::load_units_file( std::string units_path )
}
/// Suite::get_unit - returns a contained Unit identified by name attribute.
///
/// \param result - the unit type receiving the unit's value
/// \param provided_name - The name of the unit being fetched.
/**
* @brief Returns a contained Unit identified by the `provided_name` attribute.
*
* @param result The unit type that receives the unit's value.
* @param provided_name The name of the unit being fetched.
*
* @throws SuiteException if the unit with the specified name is not found.
*/
void Suite::get_unit(Unit & result, std::string provided_name)
{
bool foundMatch = false;
@ -203,7 +225,6 @@ void Suite::get_unit(Unit & result, std::string provided_name)
if (! foundMatch )
{
this->slog.log( E_FATAL, "Unit name \"" + provided_name + "\" was referenced but not defined!" );
throw SuiteException( "Undefined unit in use." );
throw SuiteException( "Undefined unit referenced." );
}
}
}

85
src/suite/Suite.h Normal file
View File

@ -0,0 +1,85 @@
/*
Rex - A configuration management and workflow automation tool that
compiles and runs in minimal environments.
© SILO GROUP and Chris Punches, 2020.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef REX_SUITE_H
#define REX_SUITE_H
#include <vector>
#include "../json_support/JSON.h"
#include "../logger/Logger.h"
#include "Unit.h"
#include "../misc/helpers.h"
#include <string.h>
#include <syslog.h>
#include <sys/stat.h>
#include <dirent.h>
/**
* @class Suite
* @brief Loads unit definition files and provides access to the unit definitions.
*
* The Suite class extends the JSON_Loader class and loads unit definition files. It stores the valid unit definitions
* in the `units` vector. The `get_unit` method can be used to retrieve a specific unit by name. The `load_units_file`
* method is used to load a unit definition file and add valid unit definitions to the `units` vector. The
* `get_units_from_dir` method is used to get a list of unit definition files from a directory. The `LOG_LEVEL` and
* `slog` member variables are used to control logging.
*/
class Suite: public JSON_Loader
{
protected:
/// storage for the definitions we are amassing from the unit definition files
std::vector<Unit> units;
public:
/**
* @brief Constructor for Suite class.
*
* @param LOG_LEVEL The logging level to use.
*/
Suite( int LOG_LEVEL );
/**
* @brief Load a unit definitions file and add valid unit definitions to `units` vector.
*
* @param filename The path to the unit definitions file.
*/
void load_units_file( std::string filename );
/**
* @brief Retrieve a unit by name.
*
* @param result The Unit object that will be set to the unit with the provided name.
* @param provided_name The name of the unit to retrieve.
*/
void get_unit(Unit & result, std::string provided_name);
private:
/**
* @brief Get a list of unit definition files from a directory.
*
* @param files A pointer to a vector of strings that will be set to the list of unit definition files.
* @param path The path to the directory containing the unit definition files.
*/
void get_units_from_dir( std::vector<std::string> * files, std::string path );
private:
/// The logging level to use.
int LOG_LEVEL;
/// A logger for logging messages.
Logger slog;
};
#endif //REX_SUITE_H

420
src/suite/Unit.cpp Normal file
View File

@ -0,0 +1,420 @@
/*
Rex - A configuration management and workflow automation tool that
compiles and runs in minimal environments.
© SILO GROUP and Chris Punches, 2020.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "Unit.h"
/**
* @class UnitException
* @brief Exception class for Unit related errors.
*
* This class provides an exception type for Unit related errors.
*/
class UnitException: public std::exception
{
public:
/**
* @brief Constructor for UnitException (C strings).
*
* @param message C-style string error message. The string contents are copied upon construction.
* Responsibility for deleting the char* lies with the caller.
*/
explicit UnitException(const char* message): msg_(message) {}
/**
* @brief Constructor for UnitException (C++ STL strings).
*
* @param message The error message.
*/
explicit UnitException(const std::string& message): msg_(message) {}
/**
* @brief Destructor for UnitException.
*
* Virtual to allow for subclassing.
*/
virtual ~UnitException() throw (){}
/**
* @brief Returns a pointer to the (constant) error description.
*
* @return A pointer to a const char*. The underlying memory is in posession of the Exception object.
* Callers must not attempt to free the memory.
*/
virtual const char* what() const throw () { return msg_.c_str(); }
protected:
/**
* @brief Error message.
*/
std::string msg_;
};
/**
* @class Unit
* @brief Definition of an automation task.
*
* The Unit is a definition of an automation task. Each Unit has:
* - name, used for identification and retrieval.
* - target, which is the filepath of an executable to trigger.
* - output, which is the desired output of the execution of target to STDOUT in determinations of success or failure
* (in addition to 0|non-0 exit code). If the output is set to look for "0" then it uses the exit code.
* - rectifier, which is the path to an executable in the event of a non-0 exit code or a failure to get the desired
* output.
* - required, which is used as a flag to halt or continue if rectifier does not heal the system in such a way that
* target can run successfully.
* - rectify, which is used as a flag to determine if the rectifier runs.
*
* @param LOG_LEVEL The log level to be used.
*/
Unit::Unit( int LOG_LEVEL ): JSON_Loader( LOG_LEVEL ), slog( LOG_LEVEL, "_unit_" )
{
this->LOG_LEVEL;
}
/**
* @brief Unit::load_root - Takes a JSON::Value and assigns the members to the Unit being populated.
*
* @param loader_root - The JSON::Value object to use to populate unit from. Usually supplied as the Suite's buffer
* member.
* @return - Boolean representation of success or failure.
*/
int Unit::load_root(Json::Value loader_root)
{
// TODO this needs reworked to have errmsg actually end up as a null return from json::value.get()
std::string errmsg = "SOMETHING WENT TERRIBLY WRONG IN PARSING";
// TODO this pattern is 'working but broken'. need to use a datastructure for required members and iterate
// do NOT replace this with a switch case pattern
if ( loader_root.isMember("name") )
{ this->name = loader_root.get("name", errmsg).asString(); } else
throw UnitException("No 'name' attribute specified when loading a unit.");
if ( loader_root.isMember("target") )
{ this->target = loader_root.get("target", errmsg).asString(); } else
throw UnitException("No 'target' attribute specified when loading a unit.");
if ( loader_root.isMember("is_shell_command") )
{ this->is_shell_command = loader_root.get("is_shell_command", errmsg).asBool(); } else
throw UnitException("No 'is_shell_command' attribute specified when loading a unit.");
if ( loader_root.isMember("shell_definition") )
{ this->shell_definition = loader_root.get("shell_definition", errmsg).asString(); } else
throw UnitException("No 'shell_definition' attribute specified when loading a unit.");
if ( loader_root.isMember("force_pty") )
{ this->force_pty = loader_root.get("force_pty", errmsg).asBool(); } else
throw UnitException("No 'force_pty' attribute specified when loading a unit.");
if ( loader_root.isMember("set_working_directory") )
{ this->set_working_directory = loader_root.get("set_working_directory", errmsg).asBool(); } else
throw UnitException("No 'set_working_directory' attribute specified when loading a unit.");
if ( loader_root.isMember("rectify") )
{ this->rectify = loader_root.get("rectify", errmsg).asBool(); } else
throw UnitException("No 'rectify' boolean attribute specified when loading a unit.");
if ( loader_root.isMember("rectifier") )
{ this->rectifier = loader_root.get("rectifier", errmsg).asString(); } else
throw UnitException("No 'rectifier' executable attribute specified when loading a unit.");
if ( loader_root.isMember("active") )
{ this->active = loader_root.get("active", errmsg).asBool(); } else
throw UnitException("No 'active' attribute specified when loading a unit.");
if ( loader_root.isMember("required") )
{ this->required = loader_root.get("required", errmsg).asBool(); } else
throw UnitException("No 'required' attribute specified when loading a unit.");
if ( loader_root.isMember("set_user_context") )
{ this->set_user_context = loader_root.get("set_user_context", errmsg).asBool(); } else
throw UnitException("No 'set_user_context' attribute specified when loading a unit.");
// TODO functionize this
int uid = getuid();
struct passwd * upw;
std::string errmsg_user;
// if no user field is specified then default to the currently executing user
if ( ( upw = getpwuid(uid) ) == nullptr )
{
throw UnitException( "Could not retrieve current user." );
} else {
errmsg_user = upw->pw_name;
}
// -TODO
if ( loader_root.isMember( "user" ) )
{ this->user = loader_root.get( "user", errmsg_user ).asString(); } else this->user = errmsg_user;
// TODO functionalize this
// get the current context gid as a backup value
int gid = getgid();
// declare the grp object to pull the name from once populated
struct group * grp;
// storage for backup value once retrieved
std::string errmsg_group;
// get the backup value and store it to errmsg_group
if ( ( grp = getgrgid( gid ) ) == nullptr )
{
throw UnitException("Could not retrieve current group");
} else {
errmsg_group = grp->gr_name;
}
if ( loader_root.isMember( "group" ) )
{ this->group = loader_root.get( "group", errmsg_group ).asString(); } else this->group = grp->gr_name;
if ( loader_root.isMember("supply_environment") )
{
this->supply_environment = loader_root.get("supply_environment", errmsg).asBool();
} else {
throw UnitException("No 'supply_environment' attribute specified when loading a unit.");
}
if ( loader_root.isMember("environment") )
{
this->env_vars_file = loader_root.get("environment", errmsg).asString();
} else {
throw UnitException("No 'environment' attribute specified when loading a unit.");
}
this->populated = true;
return 0;
}
/**
* @brief Retrieves the name of the unit.
*
* @return The name of the unit.
*
* @throws UnitException if the unit has not been populated.
*/
std::string Unit::get_name()
{
if ( ! this->populated ) { throw UnitException("Attempted to access an unpopulated unit."); }
return this->name;
}
/**
* @brief Retrieves the target of the unit.
*
* @return The target of the unit.
*
* @throws UnitException if the unit has not been populated.
*/
std::string Unit::get_target()
{
if ( ! this->populated ) { throw UnitException("Attempted to access an unpopulated unit."); }
return this->target;
}
/**
* @brief Retrieves whether the unit is a shell command.
*
* @return True if the unit is a shell command, false otherwise.
*
* @throws UnitException if the unit has not been populated.
*/
bool Unit::get_is_shell_command()
{
if (!this->populated) { throw UnitException("Attempted to access an unpopulated unit."); }
return this->is_shell_command;
}
/**
* @brief Retrieves the shell definition of the unit.
*
* @return The shell definition of the unit.
*
* @throws UnitException if the unit has not been populated.
*/
std::string Unit::get_shell_definition() {
if (!this->populated) { throw UnitException("Attempted to access an unpopulated unit."); }
return this->shell_definition;
}
/**
* @brief Retrieves whether the unit requires a PTY.
*
* @return True if the unit requires a PTY, false otherwise.
*
* @throws UnitException if the unit has not been populated.
*/
bool Unit::get_force_pty() {
if (!this->populated) { throw UnitException("Attempted to access an unpopulated unit."); }
return this->force_pty;
}
/**
* @brief Retrieves whether the working directory should be set for the unit.
*
* @return True if the working directory should be set, false otherwise.
*
* @throws UnitException if the unit has not been populated.
*/
bool Unit::get_set_working_directory() {
if (!this->populated) { throw UnitException("Attempted to access an unpopulated unit."); }
return this->set_working_directory;
}
/**
* @brief Retrieves the working directory of the unit.
*
* @return The working directory of the unit.
*
* @throws UnitException if the unit has not been populated.
*/
std::string Unit::get_working_directory() {
if (!this->populated) { throw UnitException("Attempted to access an unpopulated unit."); }
return this->working_directory;
}
/**
* @brief Retrieves the rectification status of the unit.
*
* @return The rectification status of the unit.
*
* @throws UnitException if the unit has not been populated.
*/
bool Unit::get_rectify() {
if (!this->populated) { throw UnitException("Attempted to access an unpopulated unit."); }
return this->rectify;
}
/**
* @brief Retrieves the rectifier of the unit.
*
* @return The rectifier of the unit.
*
* @throws UnitException if the unit has not been populated.
*/
std::string Unit::get_rectifier()
{
if ( ! this->populated ) { throw UnitException("Attempted to access an unpopulated unit."); }
return this->rectifier;
}
/**
* @brief Retrieves the armed status of the unit.
*
* @return The armed status of the unit.
*
* @throws UnitException if the unit has not been populated.
*/
bool Unit::get_active()
{
if ( ! this->populated ) { throw UnitException("Attempted to access an unpopulated unit."); }
return this->active;
}
/**
* @brief Retrieves the requirement status of the unit.
*
* @return The requirement status of the unit.
*
* @throws UnitException if the unit has not been populated.
*/
bool Unit::get_required()
{
if ( ! this->populated ) { throw UnitException("Attempted to access an unpopulated unit."); }
return this->required;
}
/**
* @brief Retrieves whether the user context should be set for the unit.
*
* @return True if the user context should be set, false otherwise.
*
* @throws UnitException if the unit has not been populated.
*/
bool Unit::get_set_user_context() {
if (!this->populated) { throw UnitException("Attempted to access an unpopulated unit."); }
return this->set_user_context;
}
/**
* @brief Retrieves the user context for the unit.
*
* @return The string value of the user name.
*
* @throws UnitException if the unit has not been populated.
*/
std::string Unit::get_user()
{
if ( ! this->populated ) { throw UnitException("Attempted to access an unpopulated unit."); }
return this->user;
}
/**
* @brief Retrieves the group context for the unit.
*
* @return The string value of the group name.
*
* @throws UnitException if the unit has not been populated.
*/
std::string Unit::get_group()
{
if ( ! this->populated ) { throw UnitException("Attempted to access an unpopulated unit."); }
return this->group;
}
/**
* @brief Retrieves whether the environment should be supplied to the unit.
*
* @return True if the environment should be supplied, false otherwise.
*
* @throws UnitException if the unit has not been populated.
*/
bool Unit::get_supply_environment() {
if (!this->populated) { throw UnitException("Attempted to access an unpopulated unit."); }
return this->supply_environment;
}
/**
* @brief Retrieves the environment file for the unit.
*
* @return The environment file for the unit.
*
* @throws UnitException if the unit has not been populated.
*/
std::string Unit::get_environment_file()
{
if ( ! this->populated ) { throw UnitException("Attempted to access an unpopulated unit."); }
return this->env_vars_file;
}

124
src/suite/Unit.h Normal file
View File

@ -0,0 +1,124 @@
/*
Rex - A configuration management and workflow automation tool that
compiles and runs in minimal environments.
© SILO GROUP and Chris Punches, 2020.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef REX_UNIT_H
#define REX_UNIT_H
#include <string>
#include "../json_support/JSON.h"
#include "../logger/Logger.h"
#include <iostream>
#include <fstream>
#include <cstdlib>
#include <stdexcept>
#include <pwd.h>
#include <grp.h>
/*
* Unit is a type that represents a safely deserialized JSON object which defines what actions are taken as rex
* iterates through it's Tasks in it's given Plan. They only define the behaviour on execution, while the tasks define
* which Units are executed and in what order (and which Units a given Task depends on.
*/
class Unit: JSON_Loader
{
private:
// the name of the test
std::string name;
// the path of the executable this test executes when run
std::string target;
// bool to indicate if the target is a shell command or not
bool is_shell_command;
// the shell profile to use defined in the shell definitions file
std::string shell_definition;
// bool to indicate if the target should be run in a pty
bool force_pty;
// bool to indicate if the working directory should be set for this execution
bool set_working_directory;
// the working directory to set for this execution
std::string working_directory;
// indicator of whether the rectifier executable should be run on test failures.
// if rectifier exits on non-zero return code, it should be trigger the behaviour indicated by required
bool rectify;
// the path of the executable this test runs when the target executable fails to produce output or a 0 exit code.
std::string rectifier;
// an indicator of whether the test is active or not
// this is used as a way to give definers a way to force executors to edit arbitrary fields or prevent
// execution of potentially dangerous or intrusive tests
bool active;
// an indicator of whether or not this test is required to pass.
// intended to be used as a flag to halt execution of further tests on failure
bool required;
// an indicator of whether or not the user context should be set for this execution
bool set_user_context;
// user to run process as.
// not intended for protected accounts, handle your own security
std::string user;
// group to run process as.
// not intended for protected accounts, handle your own security
std::string group;
// an indicator of whether or not the environment should be set for this execution
// REQUIRES this->is_shell_command == true
bool supply_environment;
// the path to a file containing environment variables or functions to be set for this execution
std::string env_vars_file;
public:
Unit( int LOG_LEVEL );
// loads a serialized jason::value object as a unit
int load_root( Json::Value loader_root );
// getters
std::string get_name();
std::string get_target();
bool get_is_shell_command();
std::string get_shell_definition();
bool get_force_pty();
bool get_set_working_directory();
std::string get_working_directory();
bool get_rectify();
std::string get_rectifier();
bool get_active();
bool get_required();
bool get_set_user_context();
std::string get_user();
std::string get_group();
bool get_supply_environment();
std::string get_environment_file();
private:
int LOG_LEVEL;
Logger slog;
};
#endif //REX_UNIT_H

View File

@ -1,7 +0,0 @@
{
"execution_context_override": true,
"execution_context": "/home/bagira/development/internal/Rex/test",
"units_path": "units/",
"logs_path": "logs/",
"config_version": "4"
}

View File

@ -1,9 +0,0 @@
{
"plan": [
{ "name": "independent test 1", "dependencies": [ null ] },
{ "name": "independent test 2", "dependencies": [ null ] },
{ "name": "dependent test", "dependencies": [ "independent test 1" ] },
{ "name": "curses dialog", "dependencies": [ "independent test 1" ] },
{ "name": "fail", "dependencies": [ null ] }
]
}

View File

@ -1,69 +0,0 @@
{
"units": [
{
"name": "independent test 1",
"target": "components/independent_test_1.bash --ls -s --arg",
"rectifier": "",
"active": true,
"required": true,
"log": true,
"user": "bagira",
"group": "bagira",
"rectify": false,
"shell": "/usr/bin/bash",
"environment": "environments/rex.variables"
},
{
"name": "independent test 2",
"target": "components/independent_test_2.bash --ls",
"rectifier": "",
"active": true,
"required": true,
"log": true,
"user": "bagira",
"group": "bagira",
"rectify": false,
"shell": "/usr/bin/bash",
"environment": "environments/rex.variables"
},
{
"name": "dependent test",
"target": "components/dependent_test.bash",
"rectifier": "",
"active": true,
"required": true,
"log": true,
"user": "bagira",
"group": "bagira",
"rectify": false,
"shell": "/usr/bin/env bash",
"environment": "environments/rex.variables"
},
{
"name": "curses dialog",
"target": "components/curses_dialog.bash",
"rectifier": "",
"active": true,
"required": true,
"log": false,
"user": "bagira",
"group": "bagira",
"rectify": false,
"shell": "/usr/bin/env bash",
"environment": "environments/rex.variables"
},
{
"name": "fail",
"target": "components/fail.bash",
"rectifier": "",
"active": true,
"required": false,
"log": true,
"user": "bagira",
"group": "bagira",
"rectify": false,
"shell": "/usr/bin/env bash",
"environment": "environments/rex.variables"
}
]
}