From 330bdf9941614cfef87558f2eecfa0c5fcddfa98 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sat, 10 May 2025 19:44:28 +1200 Subject: [PATCH] Revert to last night. --- CMakeLists.txt | 7 +- build.sh | 18 +- src/main.cpp | 1 + src/server_env_manager.cpp | 114 +++++------ src/server_env_manager.hpp | 11 +- src/service_runner.cpp | 161 ++++++++-------- src/service_runner.hpp | 2 - src/utils/execute.cpp | 182 ++++++++++++++++++ src/utils/execute.hpp | 83 ++++++++ src/utils/utils.cpp | 26 --- src/utils/utils.hpp | 2 - .../dropshell-agent/_allservicesstatus.sh | 113 +++++++++++ .../dropshell-agent/shared/_autocommands.sh | 55 ++---- 13 files changed, 547 insertions(+), 228 deletions(-) create mode 100644 src/utils/execute.cpp create mode 100644 src/utils/execute.hpp create mode 100644 templates/dropshell-agent/_allservicesstatus.sh diff --git a/CMakeLists.txt b/CMakeLists.txt index 0407167..17c7d2f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -32,9 +32,6 @@ configure_file( # Set CMAKE_MODULE_PATH to include our custom find modules set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_CURRENT_SOURCE_DIR}/cmake) -# Find required libraries -find_package(LibSSH REQUIRED) - # Auto-detect source files file(GLOB_RECURSE SOURCES "src/*.cpp") file(GLOB_RECURSE HEADERS "src/*.hpp") @@ -48,7 +45,6 @@ target_include_directories(dropshell PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/utils ${CMAKE_CURRENT_SOURCE_DIR}/src/contrib $ - ${LIBSSH_INCLUDE_DIRS} ) include(FetchContent) @@ -58,7 +54,7 @@ FetchContent_Declare( GIT_TAG v2.1.5 # ) FetchContent_MakeAvailable(libassert) -target_link_libraries(dropshell PRIVATE libassert::assert) +target_link_libraries(dropshell libassert::assert) # On windows copy libassert.dll to the same directory as the executable for your_target if(WIN32) @@ -72,7 +68,6 @@ endif() # Link libraries target_link_libraries(dropshell PRIVATE - ${LIBSSH_LIBRARIES} ) # Install targets diff --git a/build.sh b/build.sh index 3675955..0f5a919 100755 --- a/build.sh +++ b/build.sh @@ -101,13 +101,17 @@ if [ $AUTO_INSTALL = true ]; then exit 1 fi else - print_status "Installing dropshell..." - sudo make install - if [ $? -eq 0 ]; then - print_status "Installation successful!" - else - print_error "Installation failed!" - exit 1 + read -p "Do you want to install the program? (y/n) " -n 1 -r + echo + if [[ $REPLY =~ ^[Yy]$ ]]; then + print_status "Installing dropshell..." + sudo make install + if [ $? -eq 0 ]; then + print_status "Installation successful!" + else + print_error "Installation failed!" + exit 1 + fi fi fi diff --git a/src/main.cpp b/src/main.cpp index f503001..b19e33b 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -108,6 +108,7 @@ void printversion() { int main(int argc, char* argv[]) { HAPPYEXIT("hash", hash_demo_raw(safearg(argc,argv,2))) + HAPPYEXIT("makesafecmd", std::cout< +#include "utils/execute.hpp" #include #include @@ -104,90 +102,94 @@ std::string server_env_manager::get_variable(const std::string& name) const { return it->second; } -// sCommand server_env_manager::construct_standard_template_run_cmd(const std::string &service_name, const std::string &command, std::vector args, bool silent) const -// { -// if (command.empty()) -// return sCommand(); +sCommand server_env_manager::construct_standard_template_run_cmd(const std::string &service_name, const std::string &command, std::vector args, bool silent) const +{ + if (command.empty()) + return sCommand(); -// std::string remote_service_template_path = remotepath::service_template(mServerName,service_name); -// std::string script_path = remote_service_template_path + "/" + command + ".sh"; + std::string remote_service_template_path = remotepath::service_template(mServerName,service_name); + std::string script_path = remote_service_template_path + "/" + command + ".sh"; -// std::map env_vars; -// if (!get_all_service_env_vars(mServerName, service_name, env_vars)) { -// std::cerr << "Error: Failed to get all service env vars for " << service_name << std::endl; -// return sCommand(); -// } + std::map env_vars; + if (!get_all_service_env_vars(mServerName, service_name, env_vars)) { + std::cerr << "Error: Failed to get all service env vars for " << service_name << std::endl; + return sCommand(); + } -// std::string argstr = ""; -// for (const auto& arg : args) { -// argstr += " " + quote(dequote(trim(arg))); -// } + std::string argstr = ""; + for (const auto& arg : args) { + argstr += " " + quote(dequote(trim(arg))); + } -// sCommand scommand(remote_service_template_path, "bash " + quote(script_path) + argstr + (silent ? " > /dev/null 2>&1" : ""), env_vars); + sCommand scommand(remote_service_template_path, "bash " + quote(script_path) + argstr + (silent ? " > /dev/null 2>&1" : ""), env_vars); -// if (scommand.empty()) -// std::cerr << "Error: Failed to construct command for " << service_name << " " << command << std::endl; + if (scommand.empty()) + std::cerr << "Error: Failed to construct command for " << service_name << " " << command << std::endl; -// return scommand; -// } + return scommand; +} bool server_env_manager::check_remote_dir_exists(const std::string &dir_path) const { - runner::runner_ssh test_runner(get_SSH_INFO(),"test",{"-d", quote(dir_path)}); - return test_runner.execute(); + sCommand scommand("test -d " + quote(dir_path)); + return execute_ssh_command(get_SSH_INFO(), scommand, cMode::Silent); } bool server_env_manager::check_remote_file_exists(const std::string& file_path) const { - runner::runner_ssh test_runner(get_SSH_INFO(),"test",{"-f",quote(file_path)}); - return test_runner.execute(); + sCommand scommand("test -f " + quote(file_path)); + return execute_ssh_command(get_SSH_INFO(), scommand, cMode::Silent); } bool server_env_manager::check_remote_items_exist(const std::vector &file_paths) const { // convert file_paths to a single string, separated by spaces std::string file_paths_str; + std::string file_names_str; for (const auto& file_path : file_paths) { file_paths_str += quote(file_path) + " "; + file_names_str += std::filesystem::path(file_path).filename().string() + " "; } // check if all items in the vector exist on the remote server, in a single command. - runner::runner_ssh test_runner(get_SSH_INFO(),"bash",{"-c","for item in " + file_paths_str + "; do test -f $item; done"}); - return test_runner.execute(); -} + sCommand scommand("for item in " + file_paths_str + "; do test -f $item; done"); -bool server_env_manager::run_remote_template_command(const std::string &service_name, const std::string &command, std::vector args, bool silent, std::map extra_env_vars, std::string * output) const -{ - std::string working_dir = remotepath::service_template(mServerName,service_name); - std::string script_path = working_dir + "/" + command + ".sh"; - std::string argstr = ""; - for (const auto& arg : args) { - argstr += " " + quote(dequote(trim(arg))); - } - - std::map env_vars; - if (!get_all_service_env_vars(mServerName, service_name, env_vars)) { - std::cerr << "Error: Failed to get all service env vars for " << service_name << std::endl; + bool okay = execute_ssh_command(get_SSH_INFO(), scommand, cMode::Silent); + if (!okay) { + std::cerr << "Error: Required items not found on remote server: " << file_names_str << std::endl; return false; } - + return true; +} + +bool server_env_manager::run_remote_template_command(const std::string &service_name, const std::string &command, std::vector args, bool silent, std::map extra_env_vars) const +{ + sCommand scommand = construct_standard_template_run_cmd(service_name, command, args, silent); + // add the extra env vars to the command for (const auto& [key, value] : extra_env_vars) - env_vars[key] = value; + scommand.add_env_var(key, value); - bool interactive = (command=="ssh"); - - ASSERT(!output || !silent); // if output is captured, silent must be false - ASSERT(!interactive || !silent); // if command is ssh, silent must be false - - if (interactive) { - runner::runner_ssh_interactive bash_runner(get_SSH_INFO(),"bash",{"-c", quote(script_path) + argstr}, working_dir, env_vars); - return bash_runner.execute(); - } else { - runner::runner_ssh bash_runner(get_SSH_INFO(),"bash",{"-c", quote(script_path) + argstr}, working_dir, env_vars, silent); - return bash_runner.execute(); - } + if (scommand.get_command_to_run().empty()) + return false; + cMode mode = (command=="ssh") ? (cMode::Interactive | cMode::RawCommand) : cMode::Silent; + return execute_ssh_command(get_SSH_INFO(), scommand, mode); } +bool server_env_manager::run_remote_template_command_and_capture_output(const std::string &service_name, const std::string &command, std::vector args, std::string &output, bool silent, std::map extra_env_vars) const +{ + sCommand scommand = construct_standard_template_run_cmd(service_name, command, args, false); + if (scommand.get_command_to_run().empty()) + return false; + + // add the extra env vars to the command + for (const auto& [key, value] : extra_env_vars) + scommand.add_env_var(key, value); + + cMode mode = cMode::CaptureOutput | cMode::RawCommand; + return execute_ssh_command(get_SSH_INFO(), scommand, mode, &output); +} + + // base64 <<< "FOO=BAR WHEE=YAY bash ./test.sh" // echo YmFzaCAtYyAnRk9PPUJBUiBXSEVFPVlBWSBiYXNoIC4vdGVzdC5zaCcK | base64 -d | bash diff --git a/src/server_env_manager.hpp b/src/server_env_manager.hpp index ca85bad..8964bbc 100644 --- a/src/server_env_manager.hpp +++ b/src/server_env_manager.hpp @@ -9,7 +9,8 @@ #include #include #include -#include "utils/runner.hpp" +#include "utils/execute.hpp" + namespace dropshell { class server_env_manager; @@ -42,7 +43,7 @@ class server_env_manager { std::string get_SSH_USER() const { return get_variable("SSH_USER"); } std::string get_SSH_PORT() const { return get_variable("SSH_PORT"); } std::string get_DROPSHELL_DIR() const { return get_variable("DROPSHELL_DIR"); } - runner::sSSHInfo get_SSH_INFO() const { return runner::sSSHInfo{get_SSH_HOST(), get_SSH_USER(), get_SSH_PORT()}; } + sSSHInfo get_SSH_INFO() const { return sSSHInfo{get_SSH_HOST(), get_SSH_USER(), get_SSH_PORT()}; } bool is_valid() const { return mValid; } std::string get_server_name() const { return mServerName; } @@ -53,10 +54,12 @@ class server_env_manager { bool check_remote_items_exist(const std::vector& file_paths) const; bool run_remote_template_command(const std::string& service_name, const std::string& command, - std::vector args = {}, bool silent = false, std::map extra_env_vars = {}, std::string * output = nullptr) const; + std::vector args, bool silent, std::map extra_env_vars) const; + bool run_remote_template_command_and_capture_output(const std::string& service_name, const std::string& command, + std::vector args, std::string & output, bool silent, std::map extra_env_vars) const; private: - //sCommand construct_standard_template_run_cmd(const std::string& service_name, const std::string& command, std::vector args, bool silent) const; + sCommand construct_standard_template_run_cmd(const std::string& service_name, const std::string& command, std::vector args, bool silent) const; private: std::string mServerName; diff --git a/src/service_runner.cpp b/src/service_runner.cpp index dd2bdb9..8229443 100644 --- a/src/service_runner.cpp +++ b/src/service_runner.cpp @@ -1,3 +1,4 @@ + #include #include #include @@ -15,7 +16,7 @@ #include "services.hpp" #include "utils/directories.hpp" #include "utils/utils.hpp" -#include "utils/runner.hpp" + namespace fs = std::filesystem; @@ -52,31 +53,35 @@ bool service_runner::install(bool silent) { if (!tinfo.is_set()) return false; - - { // Create service directory - runner::runner_ssh mkdir_runner(mServerEnv.get_SSH_INFO(),"mkdir",{"-p",remotepath::service(mServer, mService)}); - if (!mkdir_runner.execute()) - { - std::cerr << "Failed to create service directory " << remotepath::service(mServer, mService) << std::endl; - return false; - } + // Create service directory + std::string remote_service_path = remotepath::service(mServer, mService); + std::string mkdir_cmd = "mkdir -p " + quote(remote_service_path); + if (!execute_ssh_command(mServerEnv.get_SSH_INFO(), sCommand(mkdir_cmd), cMode::Silent)) + { + std::cerr << "Failed to create service directory " << remote_service_path << std::endl; + return false; } - - { // Check if rsync is installed on remote host - runner::runner_ssh which_runner(mServerEnv.get_SSH_INFO(),"which",{"rsync"}); - if (!which_runner.execute()) - { - std::cerr << "rsync is not installed on the remote host" << std::endl; - return false; - } + + // Check if rsync is installed on remote host + std::string check_rsync_cmd = "which rsync > /dev/null 2>&1"; + if (!execute_ssh_command(mServerEnv.get_SSH_INFO(), sCommand(check_rsync_cmd), cMode::Silent)) + { + std::cerr << "rsync is not installed on the remote host" << std::endl; + return false; } - // make sure all shell files are executable - make_shell_files_executable(tinfo.local_template_path().string()); - // Copy template files { - if (!rsync_copy(tinfo.local_template_path().string()+"/", remotepath::service_template(mServer, mService)+"/", silent)) { + std::cout << "Copying: [LOCAL] " << tinfo.local_template_path() << std::endl << std::string(8,' ')<<"[REMOTE] " << remotepath::service_template(mServer, mService) << "/" << std::endl; + std::string rsync_cmd = "rsync --delete -zrpc -e 'ssh -p " + mServerEnv.get_SSH_PORT() + "' " + + quote(tinfo.local_template_path().string()+"/") + " "+ + mServerEnv.get_SSH_USER() + "@" + mServerEnv.get_SSH_HOST() + ":" + + quote(remotepath::service_template(mServer, mService)+"/"); + //std::cout << std::endl << rsync_cmd << std::endl << std::endl; + if (!execute_local_command(rsync_cmd, silent ? cMode::Silent : cMode::None)) + { + std::cerr << "Failed to copy template files using rsync" << std::endl; + std::cerr << "Is rsync installed on the remote host?" << std::endl; return false; } } @@ -88,8 +93,14 @@ bool service_runner::install(bool silent) { std::cerr << "Error: Service directory not found: " << local_service_path << std::endl; return false; } - - if (!rsync_copy(local_service_path + "/", remotepath::service_config(mServer,mService) + "/", silent)) { + std::cout << "Copying: [LOCAL] " << local_service_path << std::endl < service_runner::get_all_services_status(std { std::map status; + std::string command = "_allservicesstatus"; std::string service_name = "dropshell-agent"; + if (!gTemplateManager().template_command_exists(service_name, command)) + { + std::cerr << "Error: " << service_name << " does not contain the " << command << " script" << std::endl; + return status; + } server_env_manager env(server_name); if (!env.is_valid()) { @@ -285,16 +301,9 @@ std::map service_runner::get_all_services_status(std return status; } - std::string cmd_path = remotepath::service_template(server_name,service_name) + "/shared/_allservicesstatus.sh"; std::string output; - { - runner::runner_ssh_capture bash_runner(env.get_SSH_INFO(),output,cmd_path); - if (!bash_runner.execute()) - { - std::cerr << "Error: Failed to run command script at " << cmd_path << std::endl; - return status; - } - } + if (!env.run_remote_template_command_and_capture_output(service_name, command, {}, output, true, {})) + return status; std::stringstream ss(output); std::string line; @@ -398,13 +407,13 @@ bool service_runner::interactive_ssh(const std::string & server_name, const std: return false; } + sCommand scommand("bash"); server_env_manager env(server_name); if (!env.is_valid()) { std::cerr << "Error: Invalid server environment file: " << server_name << std::endl; return false; } - runner::runner_ssh_interactive ssh_runner(env.get_SSH_INFO(),command); - return ssh_runner.execute(); + return execute_ssh_command(env.get_SSH_INFO(), scommand, cMode::Interactive | cMode::RawCommand); } void service_runner::edit_server(const std::string &server_name) @@ -440,10 +449,10 @@ bool service_runner::edit_file(const std::string &file_path) const char* editor_env = std::getenv("EDITOR"); if (editor_env && std::strlen(editor_env) > 0) { - editor_cmd = std::string(editor_env); + editor_cmd = std::string(editor_env) + " " + quote(file_path); } else if (isatty(STDIN_FILENO)) { // Check if stdin is connected to a terminal if EDITOR is not set - editor_cmd = "nano"; + editor_cmd = "nano -w " + quote(file_path); } else { std::cerr << "Error: Standard input is not a terminal and EDITOR environment variable is not set." << std::endl; std::cerr << "Try setting the EDITOR environment variable (e.g., export EDITOR=nano) or run in an interactive terminal." << std::endl; @@ -452,8 +461,7 @@ bool service_runner::edit_file(const std::string &file_path) } std::cout << "Editing file: " << file_path << std::endl; - runner::runner_local_interactive editor_runner(editor_cmd,{file_path}); - return editor_runner.execute(); + return execute_local_command(editor_cmd, cMode::Interactive | cMode::RawCommand); } bool service_runner::interactive_ssh_service() @@ -559,9 +567,8 @@ bool service_runner::restore(std::string backup_file, bool silent) std::string remote_backup_file_path = remote_backups_dir + "/" + backup_file; // Copy backup file from local to server - runner::runner_local scp_runner("scp",{quote(local_backup_file_path), mServerEnv.get_SSH_USER() + "@" + mServerEnv.get_SSH_HOST() + ":" + quote(remote_backup_file_path)}); - if (!scp_runner.execute()) - { + std::string scp_cmd = "scp -P " + mServerEnv.get_SSH_PORT() + " " + quote(local_backup_file_path) + " " + mServerEnv.get_SSH_USER() + "@" + mServerEnv.get_SSH_HOST() + ":" + quote(remote_backup_file_path) + (silent ? " > /dev/null 2>&1" : ""); + if (!execute_local_command(scp_cmd, silent ? cMode::Silent : cMode::None)) { std::cerr << "Failed to copy backup file from server" << std::endl; return false; } @@ -637,14 +644,11 @@ bool service_runner::backup(bool silent) { // Create backups directory on server if it doesn't exist std::string remote_backups_dir = remotepath::backups(mServer); - if (!silent) std::cout << "Remote backups directory on "<< mServer <<": " << remote_backups_dir << std::endl; - { - runner::runner_ssh mkdir_runner(mServerEnv.get_SSH_INFO(),"mkdir",{"-p",quote(remote_backups_dir)}); - if (!mkdir_runner.execute()) - { - std::cerr << "Failed to create backups directory on server" << std::endl; - return false; - } + if (!silent) std::cout << "Remote backups directory on "<< mServer <<": " << remote_backups_dir << std::endl; + std::string mkdir_cmd = "mkdir -p " + quote(remote_backups_dir); + if (!execute_ssh_command(mServerEnv.get_SSH_INFO(), sCommand(mkdir_cmd), cMode::Silent)) { + std::cerr << "Failed to create backups directory on server" << std::endl; + return false; } // Create backups directory locally if it doesn't exist @@ -683,9 +687,10 @@ bool service_runner::backup(bool silent) { } // Copy backup file from server to local - runner::runner_local scp_runner("scp",{"-P", mServerEnv.get_SSH_PORT(), mServerEnv.get_SSH_USER() + "@" + mServerEnv.get_SSH_HOST() + ":" + quote(remote_backup_file_path), quote(local_backup_file_path)}); - if (!scp_runner.execute()) - { + std::string scp_cmd = "scp -P " + mServerEnv.get_SSH_PORT() + " " + + mServerEnv.get_SSH_USER() + "@" + mServerEnv.get_SSH_HOST() + ":" + + quote(remote_backup_file_path) + " " + quote(local_backup_file_path) + (silent ? " > /dev/null 2>&1" : ""); + if (!execute_local_command(scp_cmd, silent ? cMode::Silent : cMode::None)) { std::cerr << "Failed to copy backup file from server" << std::endl; return false; } @@ -701,20 +706,17 @@ bool service_runner::backup(bool silent) { cRemoteTempFolder::cRemoteTempFolder(const server_env_manager &server_env) : mServerEnv(server_env) { std::string p = remotepath::temp_files(server_env.get_server_name()) + "/" + random_alphanumeric_string(10); - { - runner::runner_ssh mkdir_runner(server_env.get_SSH_INFO(),"mkdir",{"-p",quote(p)}); - if (!mkdir_runner.execute()) - std::cerr << "Failed to create temp directory on server" << std::endl; - else - mPath = p; - } + std::string mkdir_cmd = "mkdir -p " + quote(p); + if (!execute_ssh_command(server_env.get_SSH_INFO(), sCommand(mkdir_cmd), cMode::Silent)) + std::cerr << "Failed to create temp directory on server" << std::endl; + else + mPath = p; } cRemoteTempFolder::~cRemoteTempFolder() { - runner::runner_ssh rm_runner(mServerEnv.get_SSH_INFO(),"rm",{"-rf",quote(mPath)}); - if (!rm_runner.execute()) - std::cerr << "Failed to remove temp directory on server" << std::endl; + std::string rm_cmd = "rm -rf " + quote(mPath); + execute_ssh_command(mServerEnv.get_SSH_INFO(), sCommand(rm_cmd), cMode::Silent); } std::string cRemoteTempFolder::path() const @@ -769,17 +771,4 @@ std::string service_runner::get_latest_backup_file(const std::string& server, co return latest_file; } -bool service_runner::rsync_copy(const std::string& local_path, const std::string& remote_path, bool silent) { - std::cout << "Copying: [LOCAL] " << local_path << std::endl << std::string(8,' ')<<"[REMOTE] " << remote_path << std::endl; - - runner::runner_local rsync_runner("rsync", {"--delete","--mkpath","-zrpc","-e",quote("ssh -p " + mServerEnv.get_SSH_PORT()),quote(local_path), - quote(mServerEnv.get_SSH_USER()+"@"+mServerEnv.get_SSH_HOST()+":"+remote_path)}); - - if (!rsync_runner.execute()) { - std::cerr << "Failed to copy files using rsync" << std::endl; - return false; - } - return true; -} - } // namespace dropshell \ No newline at end of file diff --git a/src/service_runner.hpp b/src/service_runner.hpp index 2c46625..53eeaf9 100644 --- a/src/service_runner.hpp +++ b/src/service_runner.hpp @@ -87,8 +87,6 @@ class service_runner { // edit the service configuration file void edit_service_config(); - // Helper methods - bool rsync_copy(const std::string& local_path, const std::string& remote_path, bool silent); public: // utility functions diff --git a/src/utils/execute.cpp b/src/utils/execute.cpp new file mode 100644 index 0000000..b4a7c7e --- /dev/null +++ b/src/utils/execute.cpp @@ -0,0 +1,182 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "execute.hpp" +#include "contrib/base64.hpp" +#include "utils/utils.hpp" + + +bool EXITSTATUSCHECK(int ret) { + return (ret != -1 && WIFEXITED(ret) && (WEXITSTATUS(ret) == 0)); // ret is -1 if the command failed to execute. +} + +namespace dropshell { +bool execute_local_command_interactive(const sCommand &command, bool silent) +{ + if (command.get_command_to_run().empty()) + return false; + std::string full_command = command.construct_cmd(cStyle::Raw); // Get the command string + + pid_t pid = fork(); + + if (pid == -1) { + // Fork failed + perror("fork failed"); + return false; + } else if (pid == 0) { + // Child process + std::vector commandvec = {"bash", "-c", full_command.c_str(),NULL}; + + if (!silent) { + std::cout << "Executing command: "; + for (auto & x : commandvec) std::cout << x << " "; + std::cout << std::endl; + } + + execvp(commandvec[0], const_cast(commandvec.data())); + // If execvp returns, it means an error occurred + perror("execvp failed"); + exit(EXIT_FAILURE); // Exit child process on error + } else { + // Parent process + int ret; + // Wait for the child process to complete + waitpid(pid, &ret, 0); + + return EXITSTATUSCHECK(ret); + } +} + +bool execute_local_command_and_capture_output(const sCommand& command, std::string * output, cMode mode) +{ + ASSERT(output != nullptr, "Output string must be provided"); + ASSERT(is_raw(mode), "Capture output mode requires raw command mode"); + ASSERT(!hasFlag(mode, cMode::Silent), "Silent mode is not allowed with capture output mode"); + if (command.get_command_to_run().empty()) + return false; + cStyle style = getStyle(mode); + std::string full_cmd = command.construct_cmd(style) + " 2>&1"; + FILE *pipe = popen(full_cmd.c_str(), "r"); + if (!pipe) { + return false; + } + char buffer[128]; + while (fgets(buffer, sizeof(buffer), pipe) != nullptr) { + (*output) += buffer; + } + int ret = pclose(pipe); + return EXITSTATUSCHECK(ret); +} + + + +bool execute_local_command(const sCommand & command, cMode mode, std::string * output /* = nullptr */) +{ + if (hasFlag(mode, cMode::Interactive)) { + ASSERT(! hasFlag(mode, cMode::CaptureOutput), "Interactive mode and capture output mode cannot be used together"); + ASSERT(output == nullptr, "Interactive mode and an output string cannot be used together"); + ASSERT(is_raw(mode), "Interactive mode requires raw command mode"); + + return execute_local_command_interactive(command, hasFlag(mode, cMode::Silent)); + } + + if (hasFlag(mode, cMode::CaptureOutput)) { + ASSERT(output != nullptr, "Capture output mode requires an output string to be provided"); + ASSERT(is_raw(mode), "Capture output mode requires raw command mode"); + ASSERT(!hasFlag(mode, cMode::Silent), "Silent mode is not allowed with capture output mode"); + + return execute_local_command_and_capture_output(command, output, mode); + } + + if (command.get_command_to_run().empty()) + return false; + cStyle style = getStyle(mode); + std::string full_cmd = command.construct_cmd(style) + " 2>&1" + (hasFlag(mode, cMode::Silent) ? " > /dev/null" : ""); + int ret = system(full_cmd.c_str()); + + bool ok = EXITSTATUSCHECK(ret); + if (!ok) { + std::cerr << "Error: Failed to execute command: " << std::endl; + std::cerr << full_cmd << std::endl; + } + return ok; +} + +bool execute_ssh_command(const sSSHInfo &ssh_info, const sCommand &command, cMode mode, std::string *output) +{ + if (command.get_command_to_run().empty()) + return false; + + ASSERT(!(hasFlag(mode, cMode::Interactive) && !is_raw(mode)), "Interactive mode requires raw command mode"); + ASSERT(!(hasFlag(mode, cMode::CaptureOutput) && output == nullptr), "Capture output mode must be used with an output string"); + + std::stringstream ssh_cmd; + ssh_cmd << "ssh -p " << ssh_info.port << " " << (hasFlag(mode, cMode::Interactive) ? "-tt " : "") + << ssh_info.user << "@" << ssh_info.host; + + std::string cmdstr; + if (!is_raw(mode)) + cmdstr = quote("bash -c " + command.construct_cmd(cStyle::Safe)); + else + { + std::string raw_cmd = command.construct_cmd(cStyle::Raw); + ASSERT(raw_cmd.find("'") == std::string::npos, "Raw command must not contain single quotes"); + cmdstr = "bash -c "+ halfquote(raw_cmd); + } + + sCommand ssh_command(ssh_cmd.str() + " " + cmdstr); + + bool rval = execute_local_command(ssh_command, mode, output); + + if (!rval) { + std::cerr < +#include + +namespace dropshell { + +class sCommand; + +// mode bitset +enum class cMode { + None = 0, + Interactive = 1, + Silent = 2, + CaptureOutput = 4, + RawCommand = 8 +}; +enum class cStyle { + Safe = 0, + Raw = 1 +}; + +inline cMode operator&(cMode lhs, cMode rhs) {return static_cast(static_cast(lhs) & static_cast(rhs));} +inline cMode operator+(cMode lhs, cMode rhs) {return static_cast(static_cast(lhs) | static_cast(rhs));} +inline cMode operator-(cMode lhs, cMode rhs) {return static_cast(static_cast(lhs) & ~static_cast(rhs));} +inline cMode operator|(cMode lhs, cMode rhs) {return static_cast(static_cast(lhs) | static_cast(rhs));} +inline cMode operator|=(cMode & lhs, cMode rhs) {return lhs = lhs | rhs;} +inline bool hasFlag(cMode mode, cMode flag) {return (mode & flag) == flag;} +inline bool is_safe(cStyle style) { return style == cStyle::Safe; } +inline bool is_raw(cStyle style) { return style == cStyle::Raw; } +inline bool is_raw(cMode mode) { return hasFlag(mode, cMode::RawCommand); } +inline cStyle getStyle(cMode mode) { return is_raw(mode) ? cStyle::Raw : cStyle::Safe; } + + +typedef struct sSSHInfo { + std::string host; + std::string user; + std::string port; +} sSSHInfo; + +bool execute_local_command(const sCommand & command, cMode mode = cMode::None, std::string * output = nullptr); +bool execute_ssh_command(const sSSHInfo & ssh_info, const sCommand & command, cMode mode = cMode::None, std::string * output = nullptr); + +std::string makesafecmd(const std::string& command); + + +// ------------------------------------------------------------------------------------------------ + + + +// class to hold a command to run on the remote server. +class sCommand { + public: + sCommand(std::string directory_to_run_in, std::string command_to_run, const std::map & env_vars) : + mDir(directory_to_run_in), mCmd(command_to_run), mVars(env_vars) {} + sCommand(std::string command_to_run) : + mDir(""), mCmd(command_to_run), mVars({}) {} + sCommand() : mDir(""), mCmd(""), mVars({}) {} + + std::string get_directory_to_run_in() const { return mDir; } + std::string get_command_to_run() const { return mCmd; } + const std::map& get_env_vars() const { return mVars; } + + void add_env_var(const std::string& key, const std::string& value) { mVars[key] = value; } + + + std::string construct_cmd(cStyle style) const; + + bool empty() const { return mCmd.empty(); } + + private: + std::string mDir; + std::string mCmd; + std::map mVars; +}; + + +} // namespace dropshell + + + +#endif diff --git a/src/utils/utils.cpp b/src/utils/utils.cpp index 926b105..a84d905 100644 --- a/src/utils/utils.cpp +++ b/src/utils/utils.cpp @@ -312,32 +312,6 @@ std::string random_alphanumeric_string(int length) return random_string; } -void make_shell_files_executable(const std::string &dir_path) -{ // recursively make all shell files in the directory executable - - const auto desired_perms = std::filesystem::perms::owner_read | std::filesystem::perms::owner_exec | - std::filesystem::perms::group_read | std::filesystem::perms::group_exec | - std::filesystem::perms::others_read | std::filesystem::perms::others_exec; - - for (const auto& entry : std::filesystem::directory_iterator(dir_path)) { - if (entry.path().extension() == ".sh") { - // check if permissions are already set - auto currentperms = std::filesystem::status(entry.path()).permissions(); - if ((currentperms & desired_perms) == desired_perms) { - continue; - } - - // set permissions - std::cout << "Setting executable permissions for " << entry.path() << std::endl; - - std::filesystem::permissions(entry.path(), desired_perms, std::filesystem::perm_options::add); - } - else if (std::filesystem::is_directory(entry.path())) { - make_shell_files_executable(entry.path()); - } - } -} - std::string requote(std::string str) { return quote(trim(dequote(trim(str)))); } diff --git a/src/utils/utils.hpp b/src/utils/utils.hpp index 597737e..58febfb 100644 --- a/src/utils/utils.hpp +++ b/src/utils/utils.hpp @@ -41,6 +41,4 @@ std::string replace_with_environment_variables_like_bash(std::string str); std::string random_alphanumeric_string(int length); -void make_shell_files_executable(const std::string& dir_path); - } // namespace dropshell \ No newline at end of file diff --git a/templates/dropshell-agent/_allservicesstatus.sh b/templates/dropshell-agent/_allservicesstatus.sh new file mode 100644 index 0000000..52fa568 --- /dev/null +++ b/templates/dropshell-agent/_allservicesstatus.sh @@ -0,0 +1,113 @@ +#!/bin/bash + +# This script checks ALL services on the server and returns a status for each. + +# Return format is simple ENV with the following format: +# SERVICE_NAME_HEALTH=healthy|unhealthy|unknown +# SERVICE_NAME_PORTS=port1,port2,port3 + +# Get all services on the server +SCRIPT_DIR="$(dirname "$0")" + +# // DROPSHELL_DIR +# // |-- backups +# // |-- services +# // |-- service name +# // |-- config <-- this is passed as argument to all scripts +# // |-- service.env +# // |-- template +# // |-- (script files) +# // |-- config +# // |-- service.env +# // |-- (other config files for specific server&service) + +CURRENT_OUTPUT="" +CURRENT_EXIT_CODE=0 + +load_dotenv(){ + local file_path=$1 + if [ -f "${file_path}" ]; then + source "${file_path}" + fi +} + +function run_command() { + local service_path=$1 + local command=$2 + local capture_output=${3:-false} # default to false if not specified + + # check if the command is a file + if [ ! -f "${service_path}/template/${command}.sh" ]; then + return; + fi + + # run the command in a subshell to prevent environment changes + CURRENT_OUTPUT=$( + set -a + load_dotenv "${service_path}/template/_default.env" + load_dotenv "${service_path}/config/service.env" + set +a + + # update the main variables. + export CONFIG_PATH="${service_path}/config" + # SERVER is correct + export SERVICE="${SERVICE_NAME}" + + if [ "$capture_output" = "true" ]; then + # Capture and return output + bash "${service_path}/template/${command}.sh" 2>&1 + else + # Run silently and return exit code + bash "${service_path}/template/${command}.sh" > /dev/null 2>&1 + fi + ) + CURRENT_EXIT_CODE=$? +} + +function command_exists() { + local service_path=$1 + local command=$2 + if [ ! -f "${service_path}/template/${command}.sh" ]; then + return 1 + fi + return 0 +} + +# Get all services on the server +SERVICES_PATH=$(realpath "${SCRIPT_DIR}/../../") + +# Get all service names +SERVICE_NAMES=$(ls "${SERVICES_PATH}") + +# Iterate over all service names +for SERVICE_NAME in ${SERVICE_NAMES}; do + + SERVICE_PATH=$(realpath "${SERVICES_PATH}/${SERVICE_NAME}") + + #-------------------------------- + # Get the service health + if ! command_exists "${SERVICE_PATH}" "status"; then + SERVICE_HEALTH="unknown" + else + run_command "${SERVICE_PATH}" "status" "false" + if [ "${CURRENT_EXIT_CODE}" -eq 0 ]; then + SERVICE_HEALTH="healthy" + else + SERVICE_HEALTH="unhealthy" + fi + fi + + #-------------------------------- + # Get the service ports + if ! command_exists "${SERVICE_PATH}" "ports"; then + SERVICE_PORTS="" + else + run_command "${SERVICE_PATH}" "ports" "true" + SERVICE_PORTS="${CURRENT_OUTPUT}" + fi + + #-------------------------------- + # return the health and ports + echo "${SERVICE_NAME}_HEALTH=${SERVICE_HEALTH}" + echo "${SERVICE_NAME}_PORTS=${SERVICE_PORTS}" +done diff --git a/templates/dropshell-agent/shared/_autocommands.sh b/templates/dropshell-agent/shared/_autocommands.sh index 4c16179..226ec97 100755 --- a/templates/dropshell-agent/shared/_autocommands.sh +++ b/templates/dropshell-agent/shared/_autocommands.sh @@ -1,9 +1,13 @@ #!/bin/bash # This script contains the common code for the autocommands. +_check_required_env_vars "BACKUP_FILE" "TEMP_DIR" + MYID=$(id -u) MYGRP=$(id -g) +BACKUP_TEMP_PATH="$TEMP_DIR/backup" + _autocommandrun_volume() { local command="$1" local volume_name="$2" @@ -108,8 +112,7 @@ _autocommandparse() { local command="$1" shift - local backup_temp_path="$1" - shift + echo "autocommandparse: command=$command" # Extract the backup file and temp path (last two arguments) local args=("$@") @@ -129,7 +132,7 @@ _autocommandparse() { # create backup folder unique to key/value. local bfolder=$(echo "${key}_${value}" | tr -cd '[:alnum:]_-') - local targetpath="${backup_temp_path}/${bfolder}" + local targetpath="${BACKUP_TEMP_PATH}/${bfolder}" mkdir -p ${targetpath} # Key must be one of volume, path or file @@ -152,57 +155,31 @@ _autocommandparse() { autocreate() { - _check_required_env_vars "TEMP_DIR" - - local create_temp_path="$TEMP_DIR/create" - mkdir -p "$create_temp_path" - _autocommandparse create "$create_temp_path" "$@" - rm -rf "$create_temp_path" + _autocommandparse create "$@" } autonuke() { - _check_required_env_vars "TEMP_DIR" - - local nuke_temp_path="$TEMP_DIR/nuke" - mkdir -p "$nuke_temp_path" - _autocommandparse nuke "$nuke_temp_path" "$@" - rm -rf "$nuke_temp_path" + _autocommandparse nuke "$@" } autobackup() { - _check_required_env_vars "BACKUP_FILE" "TEMP_DIR" - local backup_temp_path="$TEMP_DIR/backup" + mkdir -p "$BACKUP_TEMP_PATH" + echo "_autocommandparse [backup] [$@]" + _autocommandparse backup "$@" - mkdir -p "$backup_temp_path" - echo "_autocommandparse [backup] [$backup_temp_path] [$@]" - - # backup the files into the temp path. - _autocommandparse backup "$backup_temp_path" "$@" - - # create the backup file. - tar zcvf "$BACKUP_FILE" -C "$backup_temp_path" . - - rm -rf "$backup_temp_path" + tar zcvf "$BACKUP_FILE" -C "$BACKUP_TEMP_PATH" . } autorestore() { - _check_required_env_vars "BACKUP_FILE" "TEMP_DIR" - local backup_temp_path="$TEMP_DIR/restore" + echo "_autocommandparse [restore] [$@]" - echo "_autocommandparse [restore] [$backup_temp_path] [$@]" + mkdir -p "$BACKUP_TEMP_PATH" + tar zxvf "$BACKUP_FILE" -C "$BACKUP_TEMP_PATH" --strip-components=1 - mkdir -p "$backup_temp_path" - - # extract the backup file into the temp path. - tar zxvf "$BACKUP_FILE" -C "$backup_temp_path" --strip-components=1 - - # restore the files from the temp path. - _autocommandparse restore "$backup_temp_path" "$@" - - rm -rf "$backup_temp_path" + _autocommandparse restore "$@" }