#include "shared_commands.hpp" #include #include #include #include #include #include "utils/utils.hpp" #include "servers.hpp" #include "directories.hpp" #include "services.hpp" #include "servers.hpp" #include "templates.hpp" #include "utils/output.hpp" #include "utils/execute.hpp" #include "transwarp.hpp" namespace dropshell { namespace shared_commands { // ------------------------------------------------------------------------------------------------ // std_autocomplete : SHARED COMMAND // ------------------------------------------------------------------------------------------------ void std_autocomplete(const CommandContext &ctx) { if (ctx.args.size() == 0) { // just the command, no args yet. // list servers std::vector servers = get_configured_servers(); for (const auto &server : servers) { rawout << server.get_server_name() << std::endl; } } else if (ctx.args.size() == 1) { // list services std::vector services = get_server_services_info(ctx.args[0]); for (const auto &service : services) { rawout << service.service_name << std::endl; } } } // ------------------------------------------------------------------------------------------------ // std_autocomplete_allowall : SHARED COMMAND // ------------------------------------------------------------------------------------------------ void std_autocomplete_allowall(const CommandContext &ctx) { std_autocomplete(ctx); if (ctx.args.size() == 1) rawout << "all" << std::endl; } // ------------------------------------------------------------------------------------------------ // rsync_tree_to_remote : SHARED COMMAND // ------------------------------------------------------------------------------------------------ bool rsync_tree_to_remote( const std::string &local_path, const std::string &remote_path, const ServerConfig &server_env, bool silent, std::string user) { ASSERT(!local_path.empty() && !remote_path.empty(), "Local or remote path not specified. Can't rsync."); std::string rsync_cmd = "rsync --delete --mkpath -zrpc -e 'ssh -p " + server_env.get_SSH_PORT() + "' " + quote(local_path + "/") + " " + quote(user + "@" + server_env.get_SSH_HOST() + ":" + remote_path + "/"); return execute_local_command("", rsync_cmd, {}, nullptr, (silent ? cMode::Silent : cMode::Defaults)); } // ------------------------------------------------------------------------------------------------ // rsync_file_to_remote : SHARED COMMAND // ------------------------------------------------------------------------------------------------ bool rsync_file_to_remote( const std::string &local_path, const std::string &remote_path, const ServerConfig &server_env, bool silent, std::string user) { ASSERT(!local_path.empty() && !remote_path.empty(), "Local or remote path not specified. Can't rsync."); ASSERT(std::filesystem::is_regular_file(local_path), "Local path "+local_path+" is not a file. Can't rsync."); std::string rsync_cmd = "rsync --mkpath -zpc -e 'ssh -p " + server_env.get_SSH_PORT() + "' " + quote(local_path) + " " + quote(user + "@" + server_env.get_SSH_HOST() + ":" + remote_path); return execute_local_command("", rsync_cmd, {}, nullptr, (silent ? cMode::Silent : cMode::Defaults)); } // ------------------------------------------------------------------------------------------------ // rsync_service_config : SHARED COMMAND // Syncs both template and service config to remote server // Used by install, check-config, reload-config // ------------------------------------------------------------------------------------------------ bool rsync_service_config( const ServerConfig &server_env, const std::string &service, bool silent) { std::string server = server_env.get_server_name(); LocalServiceInfo service_info = get_service_info(server, service); if (!SIvalid(service_info)) { error << "Service information not valid for " << service << " on " << server << std::endl; return false; } std::string user = service_info.user; std::string remote_service_path = remotepath(server, user).service(service); // Ensure service directory exists std::string mkdir_cmd = "mkdir -p " + quote(remote_service_path); if (!execute_ssh_command(server_env.get_SSH_INFO(user), sCommand("", mkdir_cmd, {}), cMode::Silent)) { error << "Failed to create service directory " << remote_service_path << std::endl; return false; } // Get template info template_info tinfo = gTemplateManager().get_template_info(service_info.template_name); if (!tinfo.is_set() || !tinfo.template_valid()) { error << "Template is not valid: " << service_info.template_name << std::endl; return false; } // Copy template files if (!silent) { debug << "Copying: [LOCAL] " << tinfo.local_template_path() << std::endl << std::string(8, ' ') << "[REMOTE] " << remotepath(server, user).service_template(service) << "/" << std::endl; } if (!rsync_tree_to_remote(tinfo.local_template_path().string(), remotepath(server, user).service_template(service), server_env, silent, user)) { error << "Failed to copy template files using rsync" << std::endl; return false; } // Copy service config files if (!silent) { debug << "Copying: [LOCAL] " << localpath::service(server, service) << std::endl << std::string(8, ' ') << "[REMOTE] " << remotepath(server, user).service_config(service) << std::endl; } if (!rsync_tree_to_remote(localpath::service(server, service), remotepath(server, user).service_config(service), server_env, silent, user)) { error << "Failed to copy service config files using rsync" << std::endl; return false; } return true; } // ------------------------------------------------------------------------------------------------ // get_arch : SHARED COMMAND // ------------------------------------------------------------------------------------------------ std::string get_arch() { // determine the architecture of the system std::string arch; #ifdef __aarch64__ arch = "aarch64"; #elif __x86_64__ arch = "x86_64"; #endif return arch; } // ------------------------------------------------------------------------------------------------ // cRemoteTempFolder : SHARED CLASS // ------------------------------------------------------------------------------------------------ cRemoteTempFolder::cRemoteTempFolder(const ServerConfig &server_env, std::string user) : mServerEnv(server_env), mUser(user) { std::string p = remotepath(server_env.get_server_name(),user).temp_files() + "/" + random_alphanumeric_string(10); std::string mkdir_cmd = "mkdir -p " + quote(p); if (!execute_ssh_command(server_env.get_SSH_INFO(user), sCommand("", mkdir_cmd, {}), cMode::Silent)) error << "Failed to create temp directory on server" << std::endl; else mPath = p; } cRemoteTempFolder::~cRemoteTempFolder() { std::string rm_cmd = "rm -rf " + quote(mPath); execute_ssh_command(mServerEnv.get_SSH_INFO(mUser), sCommand("", rm_cmd, {}), cMode::Silent); } std::string cRemoteTempFolder::path() const { return mPath; } cLocalTempFolder::cLocalTempFolder() { mPath = std::filesystem::temp_directory_path() / random_alphanumeric_string(10); std::filesystem::create_directories(mPath); } cLocalTempFolder::~cLocalTempFolder() { std::filesystem::remove_all(mPath); } std::filesystem::path cLocalTempFolder::path() const { return mPath; } // ------------------------------------------------------------------------------------------------ // get_all_services_status : SHARED COMMAND // Uses all_status.sh on the remote server to get status for all services in one SSH call // ------------------------------------------------------------------------------------------------ std::map get_all_services_status(const ServerConfig & server_env, bool* agent_match) { std::map status; bool all_match = true; bool any_checked = false; for (const auto& user : server_env.get_users()) { bool user_match = false; status.merge(get_all_services_status(server_env, user.user, &user_match)); if (!user_match) all_match = false; any_checked = true; } // Only report match if we actually checked something and all matched if (agent_match) *agent_match = (any_checked && all_match); return status; } std::map get_all_services_status(const ServerConfig & server_env, std::string user, bool* agent_match) { std::map status; std::string server_name = server_env.get_server_name(); // Default to false (mismatch/unknown) - only set to true if we get explicit confirmation if (agent_match) *agent_match = false; // Run all_status.sh on the remote server to get all service statuses in one call std::string agent_path = remotepath(server_name, user).agent(); std::string script_path = agent_path + "/all_status.sh"; // Pass expected agent hash as argument for version checking std::string expected_hash = get_local_agent_hash(); if (!expected_hash.empty()) { script_path += " " + expected_hash; } std::string output; sCommand cmd(agent_path, script_path, {}); bool success = execute_ssh_command(server_env.get_SSH_INFO(user), cmd, cMode::Silent, &output); if (!success || output.empty()) { debug << "Failed to run all_status.sh on " << server_name << " for user " << user << std::endl; return status; } // Parse JSON response try { nlohmann::json json_response = nlohmann::json::parse(output); // Check agent hash match - if field is missing, agent is too old (mismatch) if (json_response.contains("agent_match") && json_response["agent_match"].is_boolean()) { bool match = json_response["agent_match"].get(); if (agent_match) *agent_match = match; } // If agent_match field is missing, leave as false (old agent = needs update) if (!json_response.contains("services") || !json_response["services"].is_array()) { debug << "Invalid JSON response from all_status.sh" << std::endl; return status; } for (const auto& service_json : json_response["services"]) { if (!service_json.contains("name") || !service_json["name"].is_string()) { continue; } std::string service_name = service_json["name"].get(); ServiceStatus service_status; // Parse status if (service_json.contains("status") && service_json["status"].is_string()) { std::string status_str = service_json["status"].get(); if (status_str == "Running") { service_status.health = HealthStatus::HEALTHY; } else if (status_str == "Stopped") { service_status.health = HealthStatus::UNHEALTHY; } else if (status_str == "Error") { service_status.health = HealthStatus::ERROR; } else { service_status.health = HealthStatus::UNKNOWN; } } else { service_status.health = HealthStatus::UNKNOWN; } // Parse ports if (service_json.contains("ports") && service_json["ports"].is_string()) { std::string ports_str = service_json["ports"].get(); std::stringstream ss(ports_str); std::string port_token; while (ss >> port_token) { // Remove any commas port_token.erase(std::remove(port_token.begin(), port_token.end(), ','), port_token.end()); try { int port = std::stoi(port_token); if (port > 0 && port <= 65535) { service_status.ports.push_back(port); } } catch (...) { // Ignore non-numeric entries } } } // Parse template hash if (service_json.contains("template_hash") && service_json["template_hash"].is_string()) { service_status.remote_template_hash = service_json["template_hash"].get(); } status[service_name] = service_status; } } catch (const nlohmann::json::parse_error& e) { debug << "Failed to parse JSON from all_status.sh: " << e.what() << std::endl; debug << "Output was: " << output << std::endl; } return status; } // ------------------------------------------------------------------------------------------------ // healthtick : SHARED COMMAND // ------------------------------------------------------------------------------------------------ std::string healthtick(const std::string &server, const std::string &service) { 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(server, service); 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; } // ------------------------------------------------------------------------------------------------ // HealthStatus2String : SHARED COMMAND // ------------------------------------------------------------------------------------------------ std::string 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:"; } // ------------------------------------------------------------------------------------------------ // is_healthy : SHARED COMMAND // ------------------------------------------------------------------------------------------------ HealthStatus is_healthy(const std::string &server, const std::string &service) { ServerConfig env(server); if (!env.is_valid()) { error << "Server service not initialized" << std::endl; return HealthStatus::ERROR; } std::string user = env.get_user_for_service(service); if (!env.check_remote_dir_exists(remotepath(server,user).service(service), user)) { return HealthStatus::NOTINSTALLED; } std::string script_path = remotepath(server,user).service_template(service) + "/status.sh"; if (!env.check_remote_file_exists(script_path, user)) { return HealthStatus::UNKNOWN; } // Run status script, does not display output. if (!env.run_remote_template_command(service, "status", {}, true, {}, NULL)) return HealthStatus::UNHEALTHY; return HealthStatus::HEALTHY; } // ------------------------------------------------------------------------------------------------ // get_ports : SHARED COMMAND // ------------------------------------------------------------------------------------------------ std::vector get_ports(const std::string &server, const std::string &service) { std::vector ports; ServerConfig env(server); if (!env.is_valid()) { error << "Server service not initialized" << std::endl; return ports; } std::string user = env.get_user_for_service(service); // Check if ports script exists std::string script_path = remotepath(server,user).service_template(service) + "/ports.sh"; if (!env.check_remote_file_exists(script_path, user)) { return ports; // No ports script, return empty } // Run ports script and capture output std::string output; if (env.run_remote_template_command(service, "ports", {}, true, {}, &output)) { // Parse the output - expecting comma-separated or newline-separated port numbers std::stringstream ss(output); std::string port_str; while (ss >> port_str) { // Remove any commas port_str.erase(std::remove(port_str.begin(), port_str.end(), ','), port_str.end()); try { int port = std::stoi(port_str); if (port > 0 && port <= 65535) ports.push_back(port); } catch (...) { // Ignore non-numeric entries } } } return ports; } // ------------------------------------------------------------------------------------------------ // healthmark : SHARED COMMAND // ------------------------------------------------------------------------------------------------ std::string healthmark(const std::string &server, const std::string &service) { HealthStatus status = is_healthy(server, service); return HealthStatus2String(status); } // ------------------------------------------------------------------------------------------------ // cBackupFileName : SHARED CLASS // ------------------------------------------------------------------------------------------------ cBackupFileName::cBackupFileName(const std::string &server, const std::string &service, const std::string &template_name) { mServer = server; mService = service; mTemplateName = template_name; // 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"); mDatetime = datetime.str(); } cBackupFileName::cBackupFileName(const std::string &filename) { // Parse the filename according to the format: // server + magic_string() + template_name + magic_string() + service + magic_string() + datetime + ".tgz" std::string name = filename; if (name.size() > 4 && name.substr(name.size() - 4) == ".tgz") name = name.substr(0, name.size() - 4); std::string sep = magic_string(); size_t first = name.find(sep); size_t second = name.find(sep, first + sep.size()); size_t third = name.find(sep, second + sep.size()); if (first == std::string::npos || second == std::string::npos || third == std::string::npos) { mServer = mService = mTemplateName = mDatetime = ""; return; } mServer = name.substr(0, first); mTemplateName = name.substr(first + sep.size(), second - (first + sep.size())); mService = name.substr(second + sep.size(), third - (second + sep.size())); mDatetime = name.substr(third + sep.size()); } std::string cBackupFileName::get_filename() const { return mServer + magic_string() + mTemplateName + magic_string() + mService + magic_string() + mDatetime + ".tgz"; } std::string cBackupFileName::get_server() const { return mServer; } std::string cBackupFileName::get_service() const { return mService; } std::string cBackupFileName::get_template_name() const { return mTemplateName; } std::string cBackupFileName::get_datetime() const { return mDatetime; } bool cBackupFileName::is_valid() const { // All fields must be non-empty, and none may contain the magic string return !mServer.empty() && !mService.empty() && !mTemplateName.empty() && !mDatetime.empty() && !has_magic_string(mServer) && !has_magic_string(mService) && !has_magic_string(mTemplateName); } // ------------------------------------------------------------------------------------------------ // scp_file_to_remote : SHARED COMMAND // ------------------------------------------------------------------------------------------------ bool scp_file_to_remote(const ServerConfig &server_env, const std::string &local_path, const std::string &remote_path, bool silent, std::string user) { if (!server_env.is_valid()) { error << "Invalid server environment" << std::endl; return false; } ASSERT(!remote_path.empty() && !local_path.empty(), "Remote or local path not specified. Can't scp."); std::string scp_cmd = "scp -P " + server_env.get_SSH_PORT() + " " + quote(local_path) + " " + user + "@" + server_env.get_SSH_HOST() + ":" + quote(remote_path) + (silent ? " > /dev/null 2>&1" : ""); return execute_local_command("", scp_cmd, {}, nullptr, (silent ? cMode::Silent : cMode::Defaults)); } // ------------------------------------------------------------------------------------------------ // scp_file_from_remote : SHARED COMMAND // ------------------------------------------------------------------------------------------------ bool scp_file_from_remote(const ServerConfig &server_env, const std::string &remote_path, const std::string &local_path, bool silent, std::string user) { if (!server_env.is_valid()) { error << "Invalid server environment" << std::endl; return false; } ASSERT(!remote_path.empty() && !local_path.empty(), "Remote or local path not specified. Can't scp."); std::string scp_cmd = "scp -P " + server_env.get_SSH_PORT() + " " + user + "@" + server_env.get_SSH_HOST() + ":" + quote(remote_path) + " " + quote(local_path) + (silent ? " > /dev/null 2>&1" : ""); return execute_local_command("", scp_cmd, {}, nullptr, (silent ? cMode::Silent : cMode::Defaults)); } } // namespace shared_commands } // namespace dropshell