Revert to last night.
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 19s

This commit is contained in:
Your Name 2025-05-10 19:44:28 +12:00
parent 5d42db7331
commit 330bdf9941
13 changed files with 547 additions and 228 deletions

View File

@ -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
$<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}/src>
${LIBSSH_INCLUDE_DIRS}
)
include(FetchContent)
@ -58,7 +54,7 @@ FetchContent_Declare(
GIT_TAG v2.1.5 # <HASH or TAG>
)
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

View File

@ -101,6 +101,9 @@ if [ $AUTO_INSTALL = true ]; then
exit 1
fi
else
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
@ -110,6 +113,7 @@ else
exit 1
fi
fi
fi
# Return to original directory
cd ..

View File

@ -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<<makesafecmd(safearg(argc,argv,2))<<std::endl)
HAPPYEXIT("version", printversion())
BOOLEXIT("test-template", gTemplateManager().test_template(safearg(argc,argv,2)))
ASSERT(safearg(argc,argv,1) != "assert", "Hello! Here is an assert.");

View File

@ -5,9 +5,7 @@
#include "templates.hpp"
#include "utils/utils.hpp"
#include "utils/json.hpp"
#include "utils/runner.hpp"
#include <libassert/assert.hpp>
#include "utils/execute.hpp"
#include <iostream>
#include <memory>
@ -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<std::string> 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<std::string> 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<std::string, std::string> 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<std::string, std::string> 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<std::string> &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<std::string> args, bool silent, std::map<std::string, std::string> 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<std::string, std::string> 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<std::string> args, bool silent, std::map<std::string, std::string> 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<std::string> args, std::string &output, bool silent, std::map<std::string, std::string> 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

View File

@ -9,7 +9,8 @@
#include <map>
#include <memory>
#include <vector>
#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<std::string>& file_paths) const;
bool run_remote_template_command(const std::string& service_name, const std::string& command,
std::vector<std::string> args = {}, bool silent = false, std::map<std::string, std::string> extra_env_vars = {}, std::string * output = nullptr) const;
std::vector<std::string> args, bool silent, std::map<std::string, std::string> extra_env_vars) const;
bool run_remote_template_command_and_capture_output(const std::string& service_name, const std::string& command,
std::vector<std::string> args, std::string & output, bool silent, std::map<std::string, std::string> extra_env_vars) const;
private:
//sCommand construct_standard_template_run_cmd(const std::string& service_name, const std::string& command, std::vector<std::string> args, bool silent) const;
sCommand construct_standard_template_run_cmd(const std::string& service_name, const std::string& command, std::vector<std::string> args, bool silent) const;
private:
std::string mServerName;

View File

@ -1,3 +1,4 @@
#include <iostream>
#include <fstream>
#include <sstream>
@ -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())
// 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 " << remotepath::service(mServer, mService) << std::endl;
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())
// 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 <<std::string(8,' ')<<"[REMOTE] " << remotepath::service_config(mServer,mService) << std::endl;
std::string rsync_cmd = "rsync --delete -zrpc -e 'ssh -p " + mServerEnv.get_SSH_PORT() + "' " +
quote(local_service_path + "/") + " "+
mServerEnv.get_SSH_USER() + "@" + mServerEnv.get_SSH_HOST() + ":" +
quote(remotepath::service_config(mServer,mService) + "/");
if (!execute_local_command(rsync_cmd, silent ? cMode::Silent : cMode::None))
{
std::cerr << "Failed to copy service files using rsync" << std::endl;
return false;
}
}
@ -130,10 +141,10 @@ bool service_runner::uninstall(bool silent) {
}
// 4. Remove the service directory from the server
{
runner::runner_ssh rm_runner(mServerEnv.get_SSH_INFO(),"rm",{"-rf",remotepath::service(mServer, mService)});
if (!rm_runner.execute())
std::cerr << "Failed to remove remote service directory at " << remotepath::service(mServer, mService) << std::endl;
std::string rm_cmd = "rm -rf " + quote(remotepath::service(mServer, mService));
if (!execute_ssh_command(mServerEnv.get_SSH_INFO(), sCommand(rm_cmd), cMode::Silent)) {
std::cerr << "Failed to remove service directory" << std::endl;
return false;
}
std::cout << "Service " << mService << " successfully uninstalled from " << mServer << std::endl;
@ -178,13 +189,12 @@ bool service_runner::fullnuke()
return false;
}
{
runner::runner_local rm_runner("rm",{"-rf",quote(local_service_path)});
if (!rm_runner.execute())
std::cerr << "Failed to remove local service directory at " << local_service_path << std::endl;
std::string rm_cmd = "rm -rf " + quote(local_service_path);
if (!execute_local_command(rm_cmd, cMode::Silent)) {
std::cerr << "Failed to remove service directory" << std::endl;
return false;
}
std::cout << "Service " << mService << " successfully fully nuked from " << mServer << std::endl;
return true;
}
@ -276,8 +286,14 @@ std::map<std::string, ServiceStatus> service_runner::get_all_services_status(std
{
std::map<std::string, ServiceStatus> 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<std::string, ServiceStatus> 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;
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;
}
@ -638,14 +645,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::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
std::string local_backups_dir = gConfig().get_local_backup_path();
@ -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::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

View File

@ -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

182
src/utils/execute.cpp Normal file
View File

@ -0,0 +1,182 @@
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <vector>
#include <iostream>
#include <string>
#include <cstdlib>
#include <sstream>
#include <libassert/assert.hpp>
#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<const char *> 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<char* const*>(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 <<std::endl<<std::endl;
std::cerr << "Error: Failed to execute ssh command: { [" << ssh_command.get_directory_to_run_in() << "], [";
std::cerr << ssh_command.get_command_to_run() << "], [";
for (const auto& env_var : ssh_command.get_env_vars()) {
std::cerr << env_var.first << "=" << env_var.second << ", ";
}
std::cerr << "] }" << std::endl;
std::cerr <<std::endl<<std::endl;
}
return rval;
}
std::string makesafecmd(const std::string &command)
{
if (command.empty())
return "";
std::string encoded = base64_encode(dequote(trim(command)));
std::string commandstr = "echo " + encoded + " | base64 -d | bash";
return commandstr;
}
std::string sCommand::construct_cmd(cStyle style) const
{
if (mCmd.empty())
return "";
std::string cdcmd;
if (!mDir.empty())
cdcmd = "cd " + quote(mDir) + " && ";
std::string cmdstr;
for (const auto& env_var : mVars) {
cmdstr += env_var.first + "=" + quote(dequote(trim(env_var.second))) + " ";
}
cmdstr += mCmd;
if (is_safe(style))
cmdstr = makesafecmd(cmdstr);
return cdcmd + cmdstr;
}
} // namespace dropshell

83
src/utils/execute.hpp Normal file
View File

@ -0,0 +1,83 @@
#ifndef EXECUTE_HPP
#define EXECUTE_HPP
#include <string>
#include <map>
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<cMode>(static_cast<int>(lhs) & static_cast<int>(rhs));}
inline cMode operator+(cMode lhs, cMode rhs) {return static_cast<cMode>(static_cast<int>(lhs) | static_cast<int>(rhs));}
inline cMode operator-(cMode lhs, cMode rhs) {return static_cast<cMode>(static_cast<int>(lhs) & ~static_cast<int>(rhs));}
inline cMode operator|(cMode lhs, cMode rhs) {return static_cast<cMode>(static_cast<int>(lhs) | static_cast<int>(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<std::string, std::string> & 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<std::string, std::string>& 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<std::string, std::string> mVars;
};
} // namespace dropshell
#endif

View File

@ -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))));
}

View File

@ -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

View File

@ -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

View File

@ -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 "$@"
}