553 lines
26 KiB
C++
553 lines
26 KiB
C++
#include "shared_commands.hpp"
|
|
#include <libassert/assert.hpp>
|
|
#include <algorithm>
|
|
#include <sstream>
|
|
#include <mutex>
|
|
#include <nlohmann/json.hpp>
|
|
#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<ServerConfig> 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<LocalServiceInfo> 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<std::string, ServiceStatus> get_all_services_status(const ServerConfig & server_env, bool* agent_match)
|
|
{
|
|
std::map<std::string, ServiceStatus> 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<std::string, ServiceStatus> get_all_services_status(const ServerConfig & server_env, std::string user, bool* agent_match)
|
|
{
|
|
std::map<std::string, ServiceStatus> 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<bool>();
|
|
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<std::string>();
|
|
ServiceStatus service_status;
|
|
|
|
// Parse status
|
|
if (service_json.contains("status") && service_json["status"].is_string()) {
|
|
std::string status_str = service_json["status"].get<std::string>();
|
|
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::string>();
|
|
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<std::string>();
|
|
}
|
|
|
|
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<int> get_ports(const std::string &server, const std::string &service)
|
|
{
|
|
std::vector<int> 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
|