#include "config.hpp" #include "service_runner.hpp" #include "server_env_manager.hpp" #include "templates.hpp" #include "services.hpp" #include "utils/directories.hpp" #include "utils/utils.hpp" #include #include #include #include #include #include #include #include namespace fs = std::filesystem; namespace dropshell { static const std::string magic_string = "-_-"; service_runner::service_runner(const std::string& server_name, const std::string& service_name) : mServerEnv(server_name), mServer(server_name), mService(service_name), mValid(false) { if (server_name.empty() || service_name.empty()) return; // Initialize server environment if (!mServerEnv.is_valid()) return; mServiceInfo = get_service_info(server_name, service_name); mService = mServiceInfo.service_name; mValid = !mServiceInfo.local_template_path.empty(); } bool service_runner::install() { maketitle("Installing " + mService + " (" + mServiceInfo.template_name + ") on " + mServer); if (!mServerEnv.is_valid()) return false; // should never hit this. // Check if template exists template_info tinfo; if (!get_template_info(mServiceInfo.template_name, tinfo)) 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 (!mServerEnv.execute_ssh_command(mkdir_cmd)) { std::cerr << "Failed to create service directory " << remote_service_path << std::endl; return false; } // Check if rsync is installed on remote host std::string check_rsync_cmd = "which rsync > /dev/null 2>&1"; if (!mServerEnv.execute_ssh_command(check_rsync_cmd)) { std::cerr << "rsync is not installed on the remote host" << std::endl; return false; } // Copy template files { 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 + "/") + " "+ 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 (!mServerEnv.execute_local_command(rsync_cmd)) { std::cerr << "Failed to copy template files using rsync" << std::endl; std::cerr << "Is rsync installed on the remote host?" << std::endl; return false; } } // Copy service files (including service.env) { std::string local_service_path = localpath::service(mServer,mService); if (local_service_path.empty() || !fs::exists(local_service_path)) { std::cerr << "Error: Service directory not found: " << local_service_path << std::endl; return false; } std::cout << "Copying: [LOCAL] " << local_service_path << std::endl < additional_args) { if (!mServerEnv.is_valid()) { std::cerr << "Error: Server service not initialized" << std::endl; return false; } template_info tinfo; if (!get_template_info(mServiceInfo.template_name, tinfo)) { std::cerr << "Error: Template '" << mServiceInfo.template_name << "' not found" << std::endl; return false; } // don't need a script for edit! if (command == "edit") { edit_service_config(); return true; } if (!template_command_exists(mServiceInfo.template_name, command)) { std::cout << "No command script for " << mServiceInfo.template_name << " : " << command << std::endl; return true; // nothing to run. } // install doesn't require anything on the server yet. if (command == "install") return install(); std::string script_path = remotepath::service_template(mServer, mService) + "/" + command + ".sh"; // Check if service directory exists if (!mServerEnv.check_remote_dir_exists(remotepath::service(mServer, mService))) { return false; } // Check if command script exists if (!mServerEnv.check_remote_file_exists(script_path)) { return false; } // Check if env file exists if (!mServerEnv.check_remote_file_exists(remotefile::service_env(mServer, mService))) { return false; } if (command == "uninstall") return uninstall(); if (command == "ssh") { interactive_ssh_service(); return true; } if (command == "restore") { if (additional_args.size() < 1) { std::cerr << "Error: restore requires a backup file:" << std::endl; std::cerr << "dropshell restore " << std::endl; return false; } return restore(additional_args[0], false); } if (command == "backup") { return backup(false); } // Run the generic command std::vector args; // not passed through yet. return mServerEnv.run_remote_template_command(mService, command, args); } std::map service_runner::get_all_services_status(std::string server_name) { std::map status; std::string command = "_allservicesstatus"; std::string service_name = "dropshell-agent"; if (!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()) { std::cerr << "Error: Invalid server environment" << std::endl; return status; } std::string output; if (!env.run_remote_template_command_and_capture_output(service_name, command, {}, output)) return status; std::stringstream ss(output); std::string line; while (std::getline(ss, line)) { std::string key, value; std::size_t pos = line.find("="); if (pos != std::string::npos) { key = dequote(trim(line.substr(0, pos))); value = dequote(trim(line.substr(pos + 1))); // decode key, it's of format SERVICENAME_[HEALTH|PORTS] std::string service_name = key.substr(0, key.find_last_of("_")); std::string status_type = key.substr(key.find_last_of("_") + 1); if (status_type == "HEALTH") { // healthy|unhealthy|unknown if (value == "healthy") status[service_name].health = HealthStatus::HEALTHY; else if (value == "unhealthy") status[service_name].health = HealthStatus::UNHEALTHY; else if (value == "unknown") status[service_name].health = HealthStatus::UNKNOWN; else status[service_name].health = HealthStatus::ERROR; } else if (status_type == "PORTS") { // port1,port2,port3 std::vector ports = string2multi(value); for (const auto& port : ports) { if (port!="unknown") status[service_name].ports.push_back(str2int(port)); } } } } return status; } HealthStatus service_runner::is_healthy() { if (!mServerEnv.is_valid()) { std::cerr << "Error: Server service not initialized" << std::endl; return HealthStatus::ERROR; } if (!mServerEnv.check_remote_dir_exists(remotepath::service(mServer, mService))) { return HealthStatus::NOTINSTALLED; } std::string script_path = remotepath::service_template(mServer, mService) + "/status.sh"; if (!mServerEnv.check_remote_file_exists(script_path)) { return HealthStatus::UNKNOWN; } // Run status script, does not display output. if (!mServerEnv.run_remote_template_command(mService, "status", {}, true)) return HealthStatus::UNHEALTHY; return HealthStatus::HEALTHY; } std::string service_runner::healthtick() { std::string green_tick = "\033[32m✓\033[0m"; std::string red_cross = "\033[31m✗\033[0m"; std::string yellow_exclamation = "\033[33m!\033[0m"; std::string unknown = "\033[37m✓\033[0m"; HealthStatus status = is_healthy(); if (status == HealthStatus::HEALTHY) return green_tick; else if (status == HealthStatus::UNHEALTHY) return red_cross; else if (status == HealthStatus::UNKNOWN) return unknown; else return yellow_exclamation; } std::string service_runner::HealthStatus2String(HealthStatus status) { if (status == HealthStatus::HEALTHY) return ":tick:"; else if (status == HealthStatus::UNHEALTHY) return ":cross:"; else if (status == HealthStatus::UNKNOWN) return ":greytick:"; else if (status == HealthStatus::NOTINSTALLED) return ":warning:"; else return ":error:"; } bool service_runner::ensure_service_dropshell_files_up_to_date() { if (!mServerEnv.is_valid()) { std::cerr << "Error: Server service not initialized" << std::endl; return false; } // check if the service template and config are up to date on the remote server. service_versions versions(mServer, mService); if (versions.remote_up_to_date()) return true; if (!versions.remote_template_is_up_to_date()) { std::cerr << "Error: Service template is not up to date on the remote server" << std::endl; return false; } if (!versions.remote_config_is_up_to_date()) { std::cerr << "Error: Service config is not up to date on the remote server" << std::endl; return false; } // TODO - actually update things! versions.update_stored_remote_versions(); return versions.remote_up_to_date(); } std::string service_runner::healthmark() { HealthStatus status = is_healthy(); return HealthStatus2String(status); } void interactive_ssh(const std::string & server_name, const std::string & command) { std::string serverpath = localpath::server(server_name); if (serverpath.empty()) { std::cerr << "Error: Server not found: " << server_name << std::endl; return; } server_env_manager env(server_name); if (!env.is_valid()) { std::cerr << "Error: Invalid server environment file: " << server_name << std::endl; return; } std::string ssh_address = env.get_SSH_HOST(); std::string ssh_user = env.get_SSH_USER(); std::string ssh_port = env.get_SSH_PORT(); std::string login = ssh_user + "@" + ssh_address; // Execute ssh with server_name and command if (command.empty()) execlp("ssh", "ssh", "-tt", login.c_str(), "-p", ssh_port.c_str(), nullptr); else execlp("ssh", "ssh", "-tt", login.c_str(), "-p", ssh_port.c_str(), command.c_str(), nullptr); // If exec returns, it means there was an error perror("ssh execution failed"); exit(EXIT_FAILURE); } void edit_server(const std::string &server_name) { std::string serverpath = localpath::server(server_name); if (serverpath.empty()) { std::cerr << "Error: Server not found: " << server_name << std::endl; return; } std::ostringstream aftertext; aftertext << "If you have changed DROPSHELL_DIR, you should manually move the files to the new location NOW.\n" << "You can ssh in to the remote server with: dropshell ssh "< used_commands = get_used_commands(mServer, mService); if (used_commands.find("ssh") == used_commands.end()) { std::cerr << "Error: "<< mService <<" does not support ssh" << std::endl; return; } std::vector args; // not passed through yet. mServerEnv.run_remote_template_command(mService, "ssh", args); } void service_runner::edit_service_config() { std::string config_file = localfile::service_env(mServer,mService); if (!fs::exists(config_file)) { std::cerr << "Error: Service config file not found: " << config_file << std::endl; return; } std::string aftertext = "To apply your changes, run:\n dropshell install " + mServer + " " + mService; edit_file(config_file, aftertext); } bool service_runner::restore(std::string backup_file, bool silent) { if (backup_file.empty()) { std::cerr << "Error: not enough arguments. dropshell restore " << std::endl; return false; } std::string local_backups_dir = localpath::backups_path(); std::string local_backup_file_path = (std::filesystem::path(local_backups_dir) / backup_file).string(); if (! std::filesystem::exists(local_backup_file_path)) { std::cerr << "Error: Backup file not found at " << local_backup_file_path << std::endl; return false; } // split the backup filename into parts based on the magic string std::vector parts = dropshell::split(backup_file, magic_string); if (parts.size() != 4) { std::cerr << "Error: Backup file format is incompatible, - in one of the names?" << std::endl; return false; } std::string backup_server_name = parts[0]; std::string backup_template_name = parts[1]; std::string backup_service_name = parts[2]; std::string backup_datetime = parts[3]; if (backup_template_name != mServiceInfo.template_name) { std::cerr << "Error: Backup template does not match service template. Can't restore." << std::endl; return false; } std::string nicedate = std::string(backup_datetime).substr(0, 10); std::cout << "Restoring " << nicedate << " backup of " << backup_template_name << " taken from "<> confirm; if (confirm != 'y') { std::cout << "Restore cancelled." << std::endl; return false; } // run the restore script std::cout << "OK, here goes..." << std::endl; { // backup existing service std::cout << "1) Backing up existing service... " << std::flush; if (!backup(true)) // silent=true { std::cerr << std::endl; std::cerr << "Error: Backup failed, restore aborted." << std::endl; std::cerr << "You can try using dropshell install "< /dev/null 2>&1" : ""); if (!mServerEnv.execute_local_command(scp_cmd)) { std::cerr << "Failed to copy backup file from server" << std::endl; return false; } mServerEnv.run_remote_template_command(mService, "restore", {remote_backup_file_path}, silent); } // healthcheck the service std::cout << "3) Healthchecking service..." << std::endl; std::string green_tick = "\033[32m✓\033[0m"; std::string red_cross = "\033[31m✗\033[0m"; bool healthy= (mServerEnv.run_remote_template_command(mService, "status", {}, silent)); if (!silent) std::cout << (healthy ? green_tick : red_cross) << " Service is " << (healthy ? "healthy" : "NOT healthy") << std::endl; return true; } bool name_breaks_backups(std::string name) { // if name contains -_-, return true return name.find("-_-") != std::string::npos; } // backup the service over ssh, using the credentials from server.env (via server_env.hpp) // 1. run backup.sh on the server // 2. create a backup file with format server-service-datetime.tgz // 3. store it in the server's DROPSHELL_DIR/backups folder // 4. copy it to the local user_dir/backups folder // ------------------------------------------------------------------------------------------------ // Backup the service. // ------------------------------------------------------------------------------------------------ bool service_runner::backup(bool silent) { auto service_info = get_service_info(mServer, mService); if (service_info.local_service_path.empty()) { std::cerr << "Error: Service not found" << std::endl; return 1; } const std::string command = "backup"; if (!template_command_exists(service_info.template_name, command)) { std::cout << "No backup script for " << service_info.template_name << std::endl; return true; // nothing to back up. } // Check if basic installed stuff is in place. std::string remote_service_template_path = remotepath::service_template(mServer, mService); std::string remote_command_script_file = remote_service_template_path + "/" + command + ".sh"; std::string remote_service_config_path = remotepath::service_config(mServer, mService); if (!mServerEnv.check_remote_items_exist({ remotepath::service(mServer, mService), remote_command_script_file, remotefile::service_env(mServer, mService)}) ) { std::cerr << "Error: Required service directories not found on remote server" << std::endl; std::cerr << "Is the service installed?" << std::endl; return false; } // 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; std::string mkdir_cmd = "mkdir -p " + quote(remote_backups_dir); if (!mServerEnv.execute_ssh_command(mkdir_cmd)) { 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 = localpath::backups_path(); if (local_backups_dir.empty()) { std::cerr << "Error: Local backups directory not found - is DropShell initialised?" << std::endl; return false; } if (!std::filesystem::exists(local_backups_dir)) std::filesystem::create_directories(local_backups_dir); // Get current datetime for backup filename auto now = std::chrono::system_clock::now(); auto time = std::chrono::system_clock::to_time_t(now); std::stringstream datetime; datetime << std::put_time(std::localtime(&time), "%Y-%m-%d_%H-%M-%S"); if (name_breaks_backups(mServer)) {std::cerr << "Error: Server name contains invalid character sequence ( -_- ) that would break backup naming scheme" << std::endl; return 1;} if (name_breaks_backups(mService)) {std::cerr << "Error: Service name contains invalid character sequence ( -_- ) that would break backup naming scheme" << std::endl; return 1;} if (name_breaks_backups(service_info.template_name)) {std::cerr << "Error: Service template name contains invalid character sequence ( -_- ) that would break backup naming scheme" << std::endl; return 1;} // Construct backup filename std::string backup_filename = mServer + magic_string + service_info.template_name + magic_string + mService + magic_string + datetime.str() + ".tgz"; std::string remote_backup_file_path = remote_backups_dir + "/" + backup_filename; std::string local_backup_file_path = (std::filesystem::path(local_backups_dir) / backup_filename).string(); // assert that the backup filename is valid - -_- appears exactly 3 times in local_backup_file_path. ASSERT(3 == count_substring(magic_string, local_backup_file_path)); // Run backup script if (!mServerEnv.run_remote_template_command(mService, command, {remote_backup_file_path}, silent)) { std::cerr << "Backup script failed on remote server: " << remote_backup_file_path << std::endl; return false; } // Copy backup file from server to local 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 (!mServerEnv.execute_local_command(scp_cmd)) { std::cerr << "Failed to copy backup file from server" << std::endl; return false; } if (!silent) { std::cout << "Backup created successfully. Restore with:"<