diff --git a/CMakeLists.txt b/CMakeLists.txt index bf3a9d8..b5ee5ab 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 ) diff --git a/Rex.cpp b/Rex.cpp index c2a90b4..41cada1 100644 --- a/Rex.cpp +++ b/Rex.cpp @@ -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 . - */ #include #include #include - -#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 { diff --git a/doc/Config.md b/doc/Config.md new file mode 100644 index 0000000..1bf7922 --- /dev/null +++ b/doc/Config.md @@ -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. \ No newline at end of file diff --git a/docs/README.md b/doc/README.md similarity index 100% rename from docs/README.md rename to doc/README.md diff --git a/doc/SUMMARY.md b/doc/SUMMARY.md new file mode 100644 index 0000000..66ff9b5 --- /dev/null +++ b/doc/SUMMARY.md @@ -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. \ No newline at end of file diff --git a/docs/design/Execution_Flow.dia b/doc/design/Execution_Flow.dia similarity index 100% rename from docs/design/Execution_Flow.dia rename to doc/design/Execution_Flow.dia diff --git a/docs/design/Execution_Flow.dia~ b/doc/design/Execution_Flow.dia~ similarity index 100% rename from docs/design/Execution_Flow.dia~ rename to doc/design/Execution_Flow.dia~ diff --git a/docs/design/Execution_Flow.png b/doc/design/Execution_Flow.png similarity index 100% rename from docs/design/Execution_Flow.png rename to doc/design/Execution_Flow.png diff --git a/docs/design/Execution_Flow.svg b/doc/design/Execution_Flow.svg similarity index 100% rename from docs/design/Execution_Flow.svg rename to doc/design/Execution_Flow.svg diff --git a/docs/design/Target Execution Flow.txt b/doc/design/Target Execution Flow.txt similarity index 100% rename from docs/design/Target Execution Flow.txt rename to doc/design/Target Execution Flow.txt diff --git a/test/components/curses_dialog.bash b/sample/components/curses_dialog.bash similarity index 100% rename from test/components/curses_dialog.bash rename to sample/components/curses_dialog.bash diff --git a/test/components/dependent_test.bash b/sample/components/dependent_test.bash similarity index 100% rename from test/components/dependent_test.bash rename to sample/components/dependent_test.bash diff --git a/test/components/fail.bash b/sample/components/fail.bash similarity index 100% rename from test/components/fail.bash rename to sample/components/fail.bash diff --git a/test/components/independent_test_1.bash b/sample/components/independent_test_1.bash similarity index 100% rename from test/components/independent_test_1.bash rename to sample/components/independent_test_1.bash diff --git a/test/components/independent_test_2.bash b/sample/components/independent_test_2.bash similarity index 100% rename from test/components/independent_test_2.bash rename to sample/components/independent_test_2.bash diff --git a/test/environments/rex.variables b/sample/environments/rex.variables old mode 100644 new mode 100755 similarity index 100% rename from test/environments/rex.variables rename to sample/environments/rex.variables diff --git a/sample/logs/2023-02-15_18:27:40.stderr.log b/sample/logs/2023-02-15_18:27:40.stderr.log new file mode 100644 index 0000000..48fe819 --- /dev/null +++ b/sample/logs/2023-02-15_18:27:40.stderr.log @@ -0,0 +1,2 @@ +lcpex: setgid failed: Operation not permitted +lcpex: Aborting: Setting GID failed: bagira/bagira diff --git a/sample/logs/2023-02-15_18:27:40.stdout.log b/sample/logs/2023-02-15_18:27:40.stdout.log new file mode 100644 index 0000000..e69de29 diff --git a/sample/logs/2023-02-15_18:29:35.stderr.log b/sample/logs/2023-02-15_18:29:35.stderr.log new file mode 100644 index 0000000..45df159 --- /dev/null +++ b/sample/logs/2023-02-15_18:29:35.stderr.log @@ -0,0 +1 @@ +/usr/bin/bash: line 1: /home/phanes/development/internal/rex-rewrite/sample/environments/rex.variables: Permission denied diff --git a/sample/logs/2023-02-15_18:29:35.stdout.log b/sample/logs/2023-02-15_18:29:35.stdout.log new file mode 100644 index 0000000..e69de29 diff --git a/sample/logs/2023-02-15_18:30:48.stderr.log b/sample/logs/2023-02-15_18:30:48.stderr.log new file mode 100644 index 0000000..45df159 --- /dev/null +++ b/sample/logs/2023-02-15_18:30:48.stderr.log @@ -0,0 +1 @@ +/usr/bin/bash: line 1: /home/phanes/development/internal/rex-rewrite/sample/environments/rex.variables: Permission denied diff --git a/sample/logs/2023-02-15_18:30:48.stdout.log b/sample/logs/2023-02-15_18:30:48.stdout.log new file mode 100644 index 0000000..e69de29 diff --git a/test/plans/atomic.plan b/sample/plans/atomic.plan similarity index 100% rename from test/plans/atomic.plan rename to sample/plans/atomic.plan diff --git a/sample/plans/test.plan b/sample/plans/test.plan new file mode 100644 index 0000000..951198e --- /dev/null +++ b/sample/plans/test.plan @@ -0,0 +1,5 @@ +{ + "plan": [ + { "name": "independent test 1", "dependencies": [ null ] } + ] +} diff --git a/sample/rex.config b/sample/rex.config new file mode 100644 index 0000000..6a7aa6b --- /dev/null +++ b/sample/rex.config @@ -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" + } +} diff --git a/sample/shells/shells.definitions b/sample/shells/shells.definitions new file mode 100644 index 0000000..92c6cb4 --- /dev/null +++ b/sample/shells/shells.definitions @@ -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" + } + ] +} diff --git a/sample/units/.new.units.swp b/sample/units/.new.units.swp new file mode 100644 index 0000000..39ec5cd Binary files /dev/null and b/sample/units/.new.units.swp differ diff --git a/sample/units/new.units b/sample/units/new.units new file mode 100644 index 0000000..187e88b --- /dev/null +++ b/sample/units/new.units @@ -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" + } + ] +} diff --git a/src/Sproc/Sproc.cpp b/src/Sproc/Sproc.cpp deleted file mode 100644 index c603bc4..0000000 --- a/src/Sproc/Sproc.cpp +++ /dev/null @@ -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 . - -*/ - -#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 ); -} diff --git a/src/Sproc/Sproc.h b/src/Sproc/Sproc.h deleted file mode 100644 index 7f0f0aa..0000000 --- a/src/Sproc/Sproc.h +++ /dev/null @@ -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 . - -*/ - -#ifndef REX_SPROCKET_H -#define REX_SPROCKET_H - -#include "../Logger/Logger.h" -#include -#include -#include "errno.h" -#include -#include -#include -#include "unistd.h" -#include -#include -#include -#include -#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 diff --git a/src/config/Config.cpp b/src/config/Config.cpp new file mode 100644 index 0000000..ad0ef24 --- /dev/null +++ b/src/config/Config.cpp @@ -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; } \ No newline at end of file diff --git a/src/config/Config.h b/src/config/Config.h new file mode 100644 index 0000000..8f3cbbc --- /dev/null +++ b/src/config/Config.h @@ -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 . + +*/ + +#ifndef REX_CONF_H +#define REX_CONF_H + +#include +#include +#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 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 + diff --git a/src/json_support/JSON.cpp b/src/json_support/JSON.cpp new file mode 100644 index 0000000..c1316b7 --- /dev/null +++ b/src/json_support/JSON.cpp @@ -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 . + +*/ +#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; +} \ No newline at end of file diff --git a/src/json_support/JSON.h b/src/json_support/JSON.h new file mode 100644 index 0000000..c9c4990 --- /dev/null +++ b/src/json_support/JSON.h @@ -0,0 +1,112 @@ +#ifndef REX_JSON_H +#define REX_JSON_H + +#include "jsoncpp/json.h" +#include +#include +#include +#include +#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 diff --git a/src/json/json-forwards.h b/src/json_support/jsoncpp/json-forwards.h similarity index 100% rename from src/json/json-forwards.h rename to src/json_support/jsoncpp/json-forwards.h diff --git a/src/json/json.h b/src/json_support/jsoncpp/json.h similarity index 100% rename from src/json/json.h rename to src/json_support/jsoncpp/json.h diff --git a/src/json/jsoncpp.cpp b/src/json_support/jsoncpp/jsoncpp.cpp similarity index 100% rename from src/json/jsoncpp.cpp rename to src/json_support/jsoncpp/jsoncpp.cpp diff --git a/src/lcpex/Contexts.cpp b/src/lcpex/Contexts.cpp new file mode 100644 index 0000000..41a8eed --- /dev/null +++ b/src/lcpex/Contexts.cpp @@ -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; +} + diff --git a/src/lcpex/Contexts.h b/src/lcpex/Contexts.h new file mode 100644 index 0000000..afb7aa7 --- /dev/null +++ b/src/lcpex/Contexts.h @@ -0,0 +1,69 @@ +#ifndef LCPEX_CONTEXTS_H +#define LCPEX_CONTEXTS_H + +#include +#include +#include +#include +#include + +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 diff --git a/src/lcpex/helpers.h b/src/lcpex/helpers.h new file mode 100644 index 0000000..5629850 --- /dev/null +++ b/src/lcpex/helpers.h @@ -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 diff --git a/src/lcpex/liblcpex.cpp b/src/lcpex/liblcpex.cpp new file mode 100644 index 0000000..4e9a8b7 --- /dev/null +++ b/src/lcpex/liblcpex.cpp @@ -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; + } + } + } +} + diff --git a/src/lcpex/liblcpex.h b/src/lcpex/liblcpex.h new file mode 100644 index 0000000..bbfea27 --- /dev/null +++ b/src/lcpex/liblcpex.h @@ -0,0 +1,122 @@ +#ifndef LCPEX_LIBLCPEX_H +#define LCPEX_LIBLCPEX_H + +#include +#include +#include +#include +#include +#include +#include +#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 diff --git a/src/lcpex/string_expansion/string_expansion.cpp b/src/lcpex/string_expansion/string_expansion.cpp new file mode 100644 index 0000000..926bc0b --- /dev/null +++ b/src/lcpex/string_expansion/string_expansion.cpp @@ -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 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; +} diff --git a/src/lcpex/string_expansion/string_expansion.h b/src/lcpex/string_expansion/string_expansion.h new file mode 100644 index 0000000..11cb713 --- /dev/null +++ b/src/lcpex/string_expansion/string_expansion.h @@ -0,0 +1,29 @@ +#ifndef LCPEX_STRING_EXPANSION_H +#define LCPEX_STRING_EXPANSION_H + +#include +#include +#include +#include + +/** + * @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 diff --git a/src/lcpex/vpty/libclpex_tty.cpp b/src/lcpex/vpty/libclpex_tty.cpp new file mode 100644 index 0000000..b340b4f --- /dev/null +++ b/src/lcpex/vpty/libclpex_tty.cpp @@ -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; + } + } + } +} + diff --git a/src/lcpex/vpty/libclpex_tty.h b/src/lcpex/vpty/libclpex_tty.h new file mode 100644 index 0000000..452078e --- /dev/null +++ b/src/lcpex/vpty/libclpex_tty.h @@ -0,0 +1,50 @@ +// +// Created by phanes on 2/6/23. +// + +#ifndef LCPEX_LIBCLPEX_TTY_H +#define LCPEX_LIBCLPEX_TTY_H + +#include +#include +#include +#include +#include +#include +#include +#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 +#include + +/** + * @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 diff --git a/src/lcpex/vpty/pty_fork_mod/pty_fork.cpp b/src/lcpex/vpty/pty_fork_mod/pty_fork.cpp new file mode 100644 index 0000000..f314507 --- /dev/null +++ b/src/lcpex/vpty/pty_fork_mod/pty_fork.cpp @@ -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() */ +} \ No newline at end of file diff --git a/src/lcpex/vpty/pty_fork_mod/pty_fork.h b/src/lcpex/vpty/pty_fork_mod/pty_fork.h new file mode 100644 index 0000000..5fbd2cb --- /dev/null +++ b/src/lcpex/vpty/pty_fork_mod/pty_fork.h @@ -0,0 +1,39 @@ +#ifndef LCPEX_PTY_FORK_H +#define LCPEX_PTY_FORK_H + +#include +#include "./pty_master_open.h" +#include +#include "./tty_functions.h" +#include + +/* 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 diff --git a/src/lcpex/vpty/pty_fork_mod/pty_master_open.cpp b/src/lcpex/vpty/pty_fork_mod/pty_master_open.cpp new file mode 100644 index 0000000..2d4f320 --- /dev/null +++ b/src/lcpex/vpty/pty_fork_mod/pty_master_open.cpp @@ -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; +} \ No newline at end of file diff --git a/src/lcpex/vpty/pty_fork_mod/pty_master_open.h b/src/lcpex/vpty/pty_fork_mod/pty_master_open.h new file mode 100644 index 0000000..29c438c --- /dev/null +++ b/src/lcpex/vpty/pty_fork_mod/pty_master_open.h @@ -0,0 +1,32 @@ +#ifndef LCPEX_PTY_MASTER_OPEN_H +#define LCPEX_PTY_MASTER_OPEN_H + +#include +#include +#include +#include +#include +#include +#include +#include + + +/** + * @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 diff --git a/src/lcpex/vpty/pty_fork_mod/tty_functions.cpp b/src/lcpex/vpty/pty_fork_mod/tty_functions.cpp new file mode 100644 index 0000000..636cb6e --- /dev/null +++ b/src/lcpex/vpty/pty_fork_mod/tty_functions.cpp @@ -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; +} \ No newline at end of file diff --git a/src/lcpex/vpty/pty_fork_mod/tty_functions.h b/src/lcpex/vpty/pty_fork_mod/tty_functions.h new file mode 100644 index 0000000..03b7e54 --- /dev/null +++ b/src/lcpex/vpty/pty_fork_mod/tty_functions.h @@ -0,0 +1,59 @@ +#ifndef LCPEX_TTY_FUNCTIONS_H +#define LCPEX_TTY_FUNCTIONS_H + + +#include +#include +#include +#include + +/** + * @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 diff --git a/src/loaders/abstract/Conf.cpp b/src/loaders/abstract/Conf.cpp deleted file mode 100644 index 1473dfa..0000000 --- a/src/loaders/abstract/Conf.cpp +++ /dev/null @@ -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 . - -*/ -#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; -} - diff --git a/src/loaders/abstract/Conf.h b/src/loaders/abstract/Conf.h deleted file mode 100644 index 35a810a..0000000 --- a/src/loaders/abstract/Conf.h +++ /dev/null @@ -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 . - -*/ - -#ifndef REX_CONF_H -#define REX_CONF_H -#include "../low_level/JSON_Loader.h" -#include -#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 diff --git a/src/loaders/abstract/Plan.cpp b/src/loaders/abstract/Plan.cpp deleted file mode 100644 index e889342..0000000 --- a/src/loaders/abstract/Plan.cpp +++ /dev/null @@ -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 . - -*/ -#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 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." ); - } - } -} - diff --git a/src/loaders/abstract/Plan.h b/src/loaders/abstract/Plan.h deleted file mode 100644 index d363996..0000000 --- a/src/loaders/abstract/Plan.h +++ /dev/null @@ -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 . - -*/ - -#ifndef REX_PLAN_H -#define REX_PLAN_H - -#include -#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 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 diff --git a/src/loaders/abstract/Suite.h b/src/loaders/abstract/Suite.h deleted file mode 100644 index b8c9216..0000000 --- a/src/loaders/abstract/Suite.h +++ /dev/null @@ -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 . - -*/ - -#ifndef REX_UNITS_H -#define REX_UNITS_H - -#include -#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 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 * files, std::string path ); - - -private: - int LOG_LEVEL; - Logger slog; -}; - -#endif //REX_UNITS_H diff --git a/src/loaders/abstract/Task.cpp b/src/loaders/abstract/Task.cpp deleted file mode 100644 index 288fdae..0000000 --- a/src/loaders/abstract/Task.cpp +++ /dev/null @@ -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 . - -*/ - -#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 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 - // ********************************************** - } -} diff --git a/src/loaders/abstract/Unit.cpp b/src/loaders/abstract/Unit.cpp deleted file mode 100644 index 5109101..0000000 --- a/src/loaders/abstract/Unit.cpp +++ /dev/null @@ -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 . - -*/ - -#include "Unit.h" -#include -#include -#include -#include -#include -#include - -/// 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; -} - diff --git a/src/loaders/abstract/Unit.h b/src/loaders/abstract/Unit.h deleted file mode 100644 index dd453e2..0000000 --- a/src/loaders/abstract/Unit.h +++ /dev/null @@ -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 . - -*/ - -/* 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 -#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 diff --git a/src/loaders/abstract/loaders.cpp b/src/loaders/abstract/loaders.cpp deleted file mode 100644 index 0b3b8c2..0000000 --- a/src/loaders/abstract/loaders.cpp +++ /dev/null @@ -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 . - -*/ - -#include "loaders.h" diff --git a/src/loaders/abstract/loaders.h b/src/loaders/abstract/loaders.h deleted file mode 100644 index e7a4778..0000000 --- a/src/loaders/abstract/loaders.h +++ /dev/null @@ -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 . - -*/ -#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 diff --git a/src/loaders/low_level/JSON_Loader.cpp b/src/loaders/low_level/JSON_Loader.cpp deleted file mode 100644 index b85effd..0000000 --- a/src/loaders/low_level/JSON_Loader.cpp +++ /dev/null @@ -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 . - -*/ - -#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; -} \ No newline at end of file diff --git a/src/loaders/low_level/JSON_Loader.h b/src/loaders/low_level/JSON_Loader.h deleted file mode 100644 index ff87350..0000000 --- a/src/loaders/low_level/JSON_Loader.h +++ /dev/null @@ -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 . - -*/ -#ifndef REX_JLOADER_H -#define REX_JLOADER_H -#include "../../json/json.h" -#include -#include -#include -#include -#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 diff --git a/src/loaders/misc/helpers.cpp b/src/loaders/misc/helpers.cpp deleted file mode 100644 index 5fb2880..0000000 --- a/src/loaders/misc/helpers.cpp +++ /dev/null @@ -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 . - -*/ - - -#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 ); - } - -} \ No newline at end of file diff --git a/src/Logger/Logger.cpp b/src/logger/Logger.cpp similarity index 90% rename from src/Logger/Logger.cpp rename to src/logger/Logger.cpp index 82849b8..8b022d4 100644 --- a/src/Logger/Logger.cpp +++ b/src/logger/Logger.cpp @@ -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 ); +} diff --git a/src/Logger/Logger.h b/src/logger/Logger.h similarity index 78% rename from src/Logger/Logger.h rename to src/logger/Logger.h index 2984494..f4c5330 100644 --- a/src/Logger/Logger.h +++ b/src/logger/Logger.h @@ -26,7 +26,7 @@ #include #include #include -#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; }; diff --git a/src/misc/helpers.cpp b/src/misc/helpers.cpp new file mode 100644 index 0000000..6d87b46 --- /dev/null +++ b/src/misc/helpers.cpp @@ -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 ); + } +} diff --git a/src/loaders/misc/helpers.h b/src/misc/helpers.h similarity index 98% rename from src/loaders/misc/helpers.h rename to src/misc/helpers.h index 42339b8..5f82907 100644 --- a/src/loaders/misc/helpers.h +++ b/src/misc/helpers.h @@ -21,6 +21,7 @@ #ifndef REX_HELPERS_H #define REX_HELPERS_H + #include #include #include @@ -50,4 +51,4 @@ std::string get_8601(); const char * command2args( std::string input_string ); -#endif //REX_HELPERS_JH +#endif //REX_HELPERS_H diff --git a/src/plan/Plan.cpp b/src/plan/Plan.cpp new file mode 100644 index 0000000..7b338ab --- /dev/null +++ b/src/plan/Plan.cpp @@ -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 . + +*/ +#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 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." ); + } + } +} diff --git a/src/plan/Plan.h b/src/plan/Plan.h new file mode 100644 index 0000000..38b9a46 --- /dev/null +++ b/src/plan/Plan.h @@ -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 . + +*/ + +#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 + +/** + * @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 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 diff --git a/src/plan/Task.cpp b/src/plan/Task.cpp new file mode 100644 index 0000000..e81dc5a --- /dev/null +++ b/src/plan/Task.cpp @@ -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 . +*/ + +#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 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 + // ********************************************** + } +} \ No newline at end of file diff --git a/src/loaders/abstract/Task.h b/src/plan/Task.h similarity index 84% rename from src/loaders/abstract/Task.h rename to src/plan/Task.h index d98a80c..1a21bff 100644 --- a/src/loaders/abstract/Task.h +++ b/src/plan/Task.h @@ -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 . - */ #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 #include -#include "../../json/json.h" -#include "Unit.h" -#include "Suite.h" -#include "Conf.h" #include -#include "../../Sproc/Sproc.h" -#include "../misc/helpers.h" +#include + 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 get_dependencies(); -private: - Logger slog; - int LOG_LEVEL; + + private: + Logger slog; + int LOG_LEVEL; }; -#endif //REX_TASK_H +#endif //REX_TASK_H \ No newline at end of file diff --git a/src/shells/shells.cpp b/src/shells/shells.cpp new file mode 100644 index 0000000..8c7aa32 --- /dev/null +++ b/src/shells/shells.cpp @@ -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; +} \ No newline at end of file diff --git a/src/shells/shells.h b/src/shells/shells.h new file mode 100644 index 0000000..2a1b721 --- /dev/null +++ b/src/shells/shells.h @@ -0,0 +1,26 @@ +#ifndef REX_SHELLS_H +#define REX_SHELLS_H + +#include "../json_support/JSON.h" +#include + +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 diff --git a/src/loaders/abstract/Suite.cpp b/src/suite/Suite.cpp similarity index 52% rename from src/loaders/abstract/Suite.cpp rename to src/suite/Suite.cpp index 67f6cca..1aa3349 100644 --- a/src/loaders/abstract/Suite.cpp +++ b/src/suite/Suite.cpp @@ -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 . - */ -#include -#include -#include -#include "Suite.h" -#include -/// 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 * 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 * 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 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." ); } -} - +} \ No newline at end of file diff --git a/src/suite/Suite.h b/src/suite/Suite.h new file mode 100644 index 0000000..d53dead --- /dev/null +++ b/src/suite/Suite.h @@ -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 . +*/ + +#ifndef REX_SUITE_H +#define REX_SUITE_H +#include +#include "../json_support/JSON.h" +#include "../logger/Logger.h" +#include "Unit.h" +#include "../misc/helpers.h" +#include +#include +#include +#include + + +/** + * @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 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 * files, std::string path ); + + + private: + /// The logging level to use. + int LOG_LEVEL; + /// A logger for logging messages. + Logger slog; +}; +#endif //REX_SUITE_H \ No newline at end of file diff --git a/src/suite/Unit.cpp b/src/suite/Unit.cpp new file mode 100644 index 0000000..142368d --- /dev/null +++ b/src/suite/Unit.cpp @@ -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 . +*/ + +#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; +} + diff --git a/src/suite/Unit.h b/src/suite/Unit.h new file mode 100644 index 0000000..eb95068 --- /dev/null +++ b/src/suite/Unit.h @@ -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 . +*/ + + +#ifndef REX_UNIT_H +#define REX_UNIT_H + +#include +#include "../json_support/JSON.h" +#include "../logger/Logger.h" +#include +#include +#include +#include +#include +#include + + +/* + * 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 \ No newline at end of file diff --git a/test/config.json b/test/config.json deleted file mode 100644 index cf657e3..0000000 --- a/test/config.json +++ /dev/null @@ -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" -} diff --git a/test/plans/test.plan b/test/plans/test.plan deleted file mode 100644 index 8d4c0f4..0000000 --- a/test/plans/test.plan +++ /dev/null @@ -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 ] } - ] -} diff --git a/test/units/all_test.units b/test/units/all_test.units deleted file mode 100644 index a332832..0000000 --- a/test/units/all_test.units +++ /dev/null @@ -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" - } - ] -}