From 9409adf4f676619a69748c6b95dfd0ea5f027a86 Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 25 Apr 2025 20:06:47 +1200 Subject: [PATCH] Integrate in agent. --- src/servers.cpp | 35 +++---- src/servers.hpp | 2 + src/service_runner.cpp | 147 +++++++++++++++++++++++----- src/service_runner.hpp | 34 +++++-- src/utils/directories.hpp | 1 - src/utils/envmanager.cpp | 73 +------------- src/utils/envmanager.hpp | 3 +- src/utils/utils.cpp | 90 ++++++++++++++++- src/utils/utils.hpp | 10 ++ templates/dropshell-agent/backup.sh | 41 ++++++++ 10 files changed, 306 insertions(+), 130 deletions(-) create mode 100644 templates/dropshell-agent/backup.sh diff --git a/src/servers.cpp b/src/servers.cpp index 9565259..19d0d31 100644 --- a/src/servers.cpp +++ b/src/servers.cpp @@ -6,6 +6,7 @@ #include "utils/directories.hpp" #include "services.hpp" #include "config.hpp" +#include "templates.hpp" #include #include #include @@ -58,28 +59,19 @@ void list_servers() { tp.add_row({"Name", "Address", "Health", "Ports"}); std::for_each(std::execution::par, servers.begin(), servers.end(), [&](const ServerInfo& server) { - std::vector ports_used; - std::string serviceticks = ""; - std::vector services = get_server_services_info(server.name); - std::for_each(std::execution::par, services.begin(), services.end(), [&](const ServiceInfo& service) { - service_runner ss; - if (ss.init(server.name, service.service_name)) - serviceticks += ss.healthmark() + " "; - else std::cout<<"Error: Failed to initialise service runner for server: ["< ports = ss.get_ports(); - ports_used.insert(ports_used.end(), ports.begin(), ports.end()); - }); - // convert ports_used to string - std::string ports_used_str = ""; - bool first = true; - for (const auto& port : ports_used) { - if (!first) { - ports_used_str += ", "; - } - ports_used_str += std::to_string(port); - first = false; + std::map status = service_runner::get_all_services_status(server.name); + + std::set ports_used; + std::string serviceticks = ""; + for (const auto& [service_name, service_status] : status) { + ports_used.insert(service_status.ports.begin(), service_status.ports.end()); + serviceticks += service_runner::HealthStatus2String(service_status.health) + " "; } + std::string ports_used_str = ""; + for (const auto& port : ports_used) + ports_used_str += std::to_string(port) + " "; + tp.add_row({server.name, server.ssh_host, serviceticks, ports_used_str}); }); tp.print(); @@ -161,5 +153,4 @@ void show_server_details(const std::string& server_name) { } // end of list services } // end of show_server_details - -} // namespace dropshell \ No newline at end of file +} // namespace dropshell \ No newline at end of file diff --git a/src/servers.hpp b/src/servers.hpp index f1d7364..94a1c7b 100644 --- a/src/servers.hpp +++ b/src/servers.hpp @@ -4,6 +4,8 @@ #include #include +#include "service_runner.hpp" // for ServiceStatus + namespace dropshell { // Server information structure diff --git a/src/service_runner.cpp b/src/service_runner.cpp index 78bfdf0..3cd5a34 100644 --- a/src/service_runner.cpp +++ b/src/service_runner.cpp @@ -49,15 +49,23 @@ bool service_runner::init(const std::string& server_name, const std::string& ser } // Helper method implementations -std::string service_runner::construct_ssh_cmd() const { +std::string service_runner::construct_ssh_cmd(const server_env &env) { std::stringstream ssh_cmd; - ssh_cmd << "ssh -p " << m_server_env->get_SSH_PORT() << " " - << m_server_env->get_SSH_USER() << "@" << m_server_env->get_SSH_HOST() << " "; + ssh_cmd << "ssh -p " << env.get_SSH_PORT() << " " + << env.get_SSH_USER() << "@" << env.get_SSH_HOST() << " "; return ssh_cmd.str(); } -bool service_runner::check_remote_dir_exists(const std::string& dir_path) const { - std::string check_dir_cmd = construct_ssh_cmd() + "'test -d " + dir_path + "'"; +std::string service_runner::construct_ssh_cmd() const +{ + if (!m_server_env) + return std::string(); + return construct_ssh_cmd(*m_server_env); +} + +bool service_runner::check_remote_dir_exists(const std::string &dir_path) const +{ + std::string check_dir_cmd = construct_ssh_cmd() + "'test -d " + quote(dir_path) + "'"; if (system(check_dir_cmd.c_str()) != 0) { std::cerr << "Error: Directory not found on remote server:" << fs::path(dir_path).filename().string() << std::endl; return false; @@ -66,7 +74,7 @@ bool service_runner::check_remote_dir_exists(const std::string& dir_path) const } bool service_runner::check_remote_file_exists(const std::string& file_path) const { - std::string check_cmd = construct_ssh_cmd() + "'test -f " + file_path + "'"; + std::string check_cmd = construct_ssh_cmd() + "'test -f " + quote(file_path) + "'"; if (system(check_cmd.c_str()) != 0) { std::cerr << "Error: File not found on remote server: " << fs::path(file_path).filename().string() << std::endl; return false; @@ -80,7 +88,7 @@ bool service_runner::check_remote_items_exist(const std::vector &fi std::string file_paths_str; std::string file_names_str; for (const auto& file_path : file_paths) { - file_paths_str += file_path + " "; + file_paths_str += quote(file_path) + " "; file_names_str += fs::path(file_path).filename().string() + " "; } // check if all items in the vector exist on the remote server, in a single command. @@ -104,6 +112,22 @@ bool service_runner::execute_local_command(const std::string& command, const std return okay; } +bool service_runner::execute_local_command_and_capture_output(const std::string &command, std::string &output) +{ + std::string full_cmd = command + " 2>&1"; + FILE *pipe = popen(full_cmd.c_str(), "r"); + if (!pipe) { + std::cerr << "Error: Failed to execute command: " << command << std::endl; + return false; + } + char buffer[128]; + while (fgets(buffer, sizeof(buffer), pipe) != nullptr) { + output += buffer; + } + pclose(pipe); + return true; +} + bool service_runner::install() { maketitle("Installing " + m_service_info.service_name + " (" + m_service_info.template_name + ") on " + m_server_name); @@ -115,7 +139,7 @@ bool service_runner::install() { return false; // Create service directory - std::string mkdir_cmd = "'mkdir -p " + mRemote_service_path + "'"; + std::string mkdir_cmd = "'mkdir -p " + quote(mRemote_service_path) + "'"; if (!execute_ssh_command(mkdir_cmd, "Failed to create service directory")) return false; @@ -128,9 +152,9 @@ bool service_runner::install() { { std::cout << "Copying template files from " << tinfo.path << " to " << mRemote_service_template_path << "/" << std::endl; std::string rsync_cmd = "rsync --delete -zrpc -e 'ssh -p " + m_server_env->get_SSH_PORT() + "' " + - tinfo.path + "/ " + + quote(tinfo.path + "/ ") + m_server_env->get_SSH_USER() + "@" + m_server_env->get_SSH_HOST() + ":" + - mRemote_service_template_path + "/"; + quote(mRemote_service_template_path+"/"); execute_local_command(rsync_cmd,"Failed to copy template files"); } @@ -143,16 +167,16 @@ bool service_runner::install() { } std::cout << "Copying service files from " << local_service_path << " to " << mRemote_service_config_path << std::endl; std::string rsync_cmd = "rsync --delete -zrpc -e 'ssh -p " + m_server_env->get_SSH_PORT() + "' " + - local_service_path + "/ " + + quote(local_service_path + "/ ") + m_server_env->get_SSH_USER() + "@" + m_server_env->get_SSH_HOST() + ":" + - mRemote_service_config_path + "/"; + quote(mRemote_service_config_path + "/"); execute_local_command(rsync_cmd,"Failed to copy service files"); } // Run install script { - std::string install_cmd = "'cd " + mRemote_service_template_path + - " && /bin/bash install.sh " + mRemote_service_config_path + "'"; + std::string install_cmd = "'cd " + quote(mRemote_service_template_path) + + " && /bin/bash install.sh " + quote(mRemote_service_config_path) + "'"; bool ok= execute_ssh_command(install_cmd, "Failed to run install script"); if (!ok) return false; @@ -179,8 +203,8 @@ bool service_runner::uninstall() { bool script_exists = check_remote_file_exists(uninstall_script); if (script_exists) { - std::string uninstall_cmd = "'cd " + mRemote_service_template_path + - " && /bin/bash _uninstall.sh " + mRemote_service_config_path + "'"; + std::string uninstall_cmd = "'cd " + quote(mRemote_service_template_path) + + " && /bin/bash "+quote(uninstall_script)+" "+quote(mRemote_service_config_path)+"'"; if (!execute_ssh_command(uninstall_cmd, "Failed to run uninstall script")) { std::cerr << "Warning: Uninstall script failed, but continuing with directory removal" << std::endl; } @@ -190,7 +214,7 @@ bool service_runner::uninstall() { } // 4. Remove the service directory from the server - std::string rm_cmd = "'rm -rf " + mRemote_service_path + "'"; + std::string rm_cmd = "'rm -rf " + quote(mRemote_service_path) + "'"; if (!execute_ssh_command(rm_cmd, "Failed to remove service directory")) { return false; } @@ -241,8 +265,8 @@ bool service_runner::run_command(const std::string& command) { return backup(); // Run the generic command - std::string run_cmd = "'cd " + mRemote_service_template_path + - " && /bin/bash " + script_path + " " + mRemote_service_config_path + "'"; + std::string run_cmd = "'cd " + quote(mRemote_service_template_path) + + " && /bin/bash "+quote(script_path)+" "+quote(mRemote_service_config_path)+"'"; return execute_ssh_command(run_cmd, "Command returned error code: " + script_path); } @@ -269,7 +293,7 @@ bool service_runner::backup() { // Create backups directory on server if it doesn't exist std::string server_backups_dir = m_server_env->get_DROPSHELL_DIR() + "/backups"; - std::string mkdir_cmd = "'mkdir -p " + server_backups_dir + "'"; + std::string mkdir_cmd = "'mkdir -p " + quote(server_backups_dir) + "'"; if (!execute_ssh_command(mkdir_cmd, "Failed to create backups directory on server")) { return false; } @@ -296,8 +320,8 @@ bool service_runner::backup() { std::string local_backup_path = (fs::path(local_backups_dir) / backup_filename).string(); // Run backup script - std::string backup_cmd = "'cd " + mRemote_service_template_path + - " && /bin/bash \""+script_path+"\" " + mRemote_service_env_file + " " + server_backup_path + "'"; + std::string backup_cmd = "'cd " + quote(mRemote_service_template_path) + + " && /bin/bash "+quote(script_path)+" "+quote(mRemote_service_env_file)+" "+quote(server_backup_path)+"'"; if (!execute_ssh_command(backup_cmd, "Backup script failed")) { return false; } @@ -305,7 +329,7 @@ bool service_runner::backup() { // Copy backup file from server to local std::string scp_cmd = "scp -P " + m_server_env->get_SSH_PORT() + " " + m_server_env->get_SSH_USER() + "@" + m_server_env->get_SSH_HOST() + ":" + - server_backup_path + " " + local_backup_path; + quote(server_backup_path) + " " + quote(local_backup_path); if (!execute_local_command(scp_cmd, "Failed to copy backup file from server")) { return false; } @@ -314,7 +338,73 @@ bool service_runner::backup() { return true; } -service_runner::HealthStatus service_runner::is_healthy() +std::map service_runner::get_all_services_status(std::string server_name) +{ + std::map status; + + std::string command = "_allservicesstatus.sh"; + std::string service_name = "dropshell-agent"; + + if (!template_command_exists(service_name, command)) + { + std::cerr << "Error: " << service_name << " does not contain the _allservicesstatus.sh script" << std::endl; + return status; + } + + std::string remote_service_path = get_remote_service_path(server_name, service_name); + std::string remote_service_config_path = get_remote_service_config_path(server_name, service_name); + std::string remote_service_template_path = get_remote_service_template_path(server_name, service_name); + std::string remote_service_env_file = get_remote_service_env_file(server_name, service_name); + + + std::string script_path = remote_service_template_path + "/" + command; + + server_env env(server_name); + if (!env.is_valid()) { + std::cerr << "Error: Invalid server environment" << std::endl; + return status; + } + + std::string ssh_cmd = construct_ssh_cmd(env) + "'cd " + quote(remote_service_template_path) + + " && /bin/bash "+quote(script_path)+" "+quote(remote_service_config_path)+"'"; + + std::string output; + if (!execute_local_command_and_capture_output(ssh_cmd, 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) + status[service_name].ports.push_back(std::stoi(port)); + } + } + } + return status; +} + +HealthStatus service_runner::is_healthy() { if (!m_server_env) { std::cerr << "Error: Server service not initialized" << std::endl; @@ -360,9 +450,8 @@ std::string service_runner::healthtick() return yellow_exclamation; } -std::string service_runner::healthmark() +std::string service_runner::HealthStatus2String(HealthStatus status) { - HealthStatus status = is_healthy(); if (status == HealthStatus::HEALTHY) return ":tick:"; else if (status == HealthStatus::UNHEALTHY) @@ -375,6 +464,12 @@ std::string service_runner::healthmark() return ":error:"; } +std::string service_runner::healthmark() +{ + HealthStatus status = is_healthy(); + return HealthStatus2String(status); +} + std::vector service_runner::get_ports() { std::vector ports; diff --git a/src/service_runner.hpp b/src/service_runner.hpp index 05790b4..c9945c4 100644 --- a/src/service_runner.hpp +++ b/src/service_runner.hpp @@ -3,6 +3,9 @@ // manage a service on a server // +#ifndef SERVICE_RUNNER_HPP +#define SERVICE_RUNNER_HPP + #include #include #include @@ -11,6 +14,19 @@ namespace dropshell { +typedef enum HealthStatus { + HEALTHY, + UNHEALTHY, + NOTINSTALLED, + ERROR, + UNKNOWN +} HealthStatus; + +typedef struct ServiceStatus { + HealthStatus health; + std::vector ports; +} ServiceStatus; + class service_runner { public: service_runner(); @@ -30,13 +46,7 @@ class service_runner { // check health of service. Silent. // 1. run status.sh on the server // 2. return the output of the status.sh script - enum class HealthStatus { - HEALTHY, - UNHEALTHY, - NOTINSTALLED, - ERROR, - UNKNOWN - }; + HealthStatus is_healthy(); // get the ports of the service @@ -47,6 +57,9 @@ class service_runner { std::string healthtick(); std::string healthmark(); + // get the status of all services on the server + static std::map get_all_services_status(std::string server_name); + static std::string HealthStatus2String(HealthStatus status); private: // install the service over ssh, using the credentials from server.env (via server_env.hpp), by: @@ -73,6 +86,7 @@ class service_runner { bool backup(); + private: std::string m_server_name; ServiceInfo m_service_info; @@ -84,12 +98,18 @@ class service_runner { std::string mRemote_service_env_file; // Helper methods + static std::string construct_ssh_cmd(const server_env &env); + std::string construct_ssh_cmd() const; bool check_remote_dir_exists(const std::string& dir_path) const; bool check_remote_file_exists(const std::string& file_path) const; bool check_remote_items_exist(const std::vector& file_paths) const; bool execute_ssh_command(const std::string& command, const std::string& error_msg) const; bool execute_local_command(const std::string& command, const std::string& error_msg) const; + static bool execute_local_command_and_capture_output(const std::string& command, std::string & output); }; + } // namespace dropshell + +#endif // SERVICE_RUNNER_HPP diff --git a/src/utils/directories.hpp b/src/utils/directories.hpp index 55123bd..492e5a8 100644 --- a/src/utils/directories.hpp +++ b/src/utils/directories.hpp @@ -39,7 +39,6 @@ namespace dropshell { std::string get_remote_service_backups_path(const std::string &server_name, const std::string &service_name); std::string get_remote_service_env_file(const std::string &server_name, const std::string &service_name); - std::string get_remote_service_env_file_parent(const std::string &server_name, const std::string &service_name); } // namespace dropshell #endif diff --git a/src/utils/envmanager.cpp b/src/utils/envmanager.cpp index 92283ba..009f799 100644 --- a/src/utils/envmanager.cpp +++ b/src/utils/envmanager.cpp @@ -1,4 +1,5 @@ #include "envmanager.hpp" +#include "utils/utils.hpp" #include #include #include @@ -97,20 +98,6 @@ void envmanager::clear_variables() { m_variables.clear(); } -std::string trim(std::string str) { - // Trim leading whitespace - str.erase(str.begin(), std::find_if(str.begin(), str.end(), [](unsigned char ch) { - return !std::isspace(ch); - })); - - // Trim trailing whitespace - str.erase(std::find_if(str.rbegin(), str.rend(), [](unsigned char ch) { - return !std::isspace(ch); - }).base(), str.end()); - - return str; -} - std::string envmanager::expand_patterns(std::string str) const { // Combined regex pattern for both ${var} and $var formats std::regex var_pattern("\\$(?:\\{([^}]+)\\}|([a-zA-Z0-9_]+))"); @@ -132,62 +119,4 @@ std::string envmanager::expand_patterns(std::string str) const { return result; } -std::string multi2string(std::vector values) -{ - std::string result; - for (const auto& value : values) { - // remove any " contained in the string value, if present - std::string quoteless_value = value; - quoteless_value.erase(std::remove(quoteless_value.begin(), quoteless_value.end(), '"'), quoteless_value.end()); - result += "\"" + trim(quoteless_value) + "\","; - } - if (!result.empty()) - result.pop_back(); // Remove the last comma - - return result; -} - -std::vector string2multi(std::string values) -{ - std::vector result; - - // Return values separated by commas, but ignore commas within quotes - bool inside_quotes = false; - std::string current_item; - - for (char c : values) { - if (c == '"') { - inside_quotes = !inside_quotes; - } else if (c == ',' && !inside_quotes) { - if (!current_item.empty()) { - // Remove quotes if present - if (current_item.front() == '"' && current_item.back() == '"') { - current_item = current_item.substr(1, current_item.length() - 2); - } - std::string final = trim(current_item); - if (!final.empty()) { - result.push_back(final); - } - current_item.clear(); - } - } else { - current_item += c; - } - } - - // Add the last item if not empty - if (!current_item.empty()) { - // Remove quotes if present - if (current_item.front() == '"' && current_item.back() == '"') { - current_item = current_item.substr(1, current_item.length() - 2); - } - std::string final = trim(current_item); - if (!final.empty()) { - result.push_back(final); - } - } - - return result; -} - } // namespace dropshell diff --git a/src/utils/envmanager.hpp b/src/utils/envmanager.hpp index b533953..a8d560b 100644 --- a/src/utils/envmanager.hpp +++ b/src/utils/envmanager.hpp @@ -45,7 +45,8 @@ class envmanager { }; // utility functions - std::string trim(std::string str); +std::string trim(std::string str); +std::string dequote(std::string str); std::string multi2string(std::vector values); std::vector string2multi(std::string values); diff --git a/src/utils/utils.cpp b/src/utils/utils.cpp index 7080bc5..c5ae985 100644 --- a/src/utils/utils.cpp +++ b/src/utils/utils.cpp @@ -3,7 +3,7 @@ #include #include #include - +#include namespace dropshell { void maketitle(const std::string& title) { @@ -42,4 +42,92 @@ bool replace_line_in_file(const std::string& file_path, const std::string& searc return true; } +std::string trim(std::string str) { + // Trim leading whitespace + str.erase(str.begin(), std::find_if(str.begin(), str.end(), [](unsigned char ch) { + return !std::isspace(ch); + })); + + // Trim trailing whitespace + str.erase(std::find_if(str.rbegin(), str.rend(), [](unsigned char ch) { + return !std::isspace(ch); + }).base(), str.end()); + + return str; +} + +std::string dequote(std::string str) +{ + if (str.length() < 2) + return str; + if (str.front() == '"' && str.back() == '"') { + return str.substr(1, str.length() - 2); + } + return str; +} + +std::string quote(std::string str) +{ + return "\""+str+"\""; +} + +std::string multi2string(std::vector values) +{ + std::string result; + for (const auto& value : values) { + // remove any " contained in the string value, if present + std::string quoteless_value = value; + quoteless_value.erase(std::remove(quoteless_value.begin(), quoteless_value.end(), '"'), quoteless_value.end()); + result += "\"" + trim(quoteless_value) + "\","; + } + if (!result.empty()) + result.pop_back(); // Remove the last comma + + return result; +} + +std::vector string2multi(std::string values) +{ + std::vector result; + + // Return values separated by commas, but ignore commas within quotes + bool inside_quotes = false; + std::string current_item; + + for (char c : values) { + if (c == '"') { + inside_quotes = !inside_quotes; + } else if (c == ',' && !inside_quotes) { + if (!current_item.empty()) { + // Remove quotes if present + if (current_item.front() == '"' && current_item.back() == '"') { + current_item = current_item.substr(1, current_item.length() - 2); + } + std::string final = trim(current_item); + if (!final.empty()) { + result.push_back(final); + } + current_item.clear(); + } + } else { + current_item += c; + } + } + + // Add the last item if not empty + if (!current_item.empty()) { + // Remove quotes if present + if (current_item.front() == '"' && current_item.back() == '"') { + current_item = current_item.substr(1, current_item.length() - 2); + } + std::string final = trim(current_item); + if (!final.empty()) { + result.push_back(final); + } + } + + return result; +} + + } // namespace dropshell \ No newline at end of file diff --git a/src/utils/utils.hpp b/src/utils/utils.hpp index 305c347..78719b5 100644 --- a/src/utils/utils.hpp +++ b/src/utils/utils.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include namespace dropshell { @@ -13,4 +14,13 @@ void maketitle(const std::string& title); bool replace_line_in_file(const std::string& file_path, const std::string& search_string, const std::string& replacement_line); + +// utility functions +std::string trim(std::string str); +std::string dequote(std::string str); +std::string quote(std::string str); +std::string multi2string(std::vector values); +std::vector string2multi(std::string values); + + } // namespace dropshell \ No newline at end of file diff --git a/templates/dropshell-agent/backup.sh b/templates/dropshell-agent/backup.sh new file mode 100644 index 0000000..ccceb35 --- /dev/null +++ b/templates/dropshell-agent/backup.sh @@ -0,0 +1,41 @@ +#!/bin/bash + +# BACKUP SCRIPT +# The backup script is OPTIONAL. +# It is used to backup the service on the server. +# It is called with: +# 1) the path to the server specific env file as the frist argument. +# 2) the path to the destination backup file as the second argument. +# If the backup file already exists, the script should exit with a message. + +source "$(dirname "$0")/_common.sh" +load_env "$1" || die "Failed to load environment variables" + +# Required environment variables +check_required_env_vars "CONTAINER_NAME" "LOCAL_DATA_FOLDER" + +# Get backup file path from second argument +BACKUP_FILE="$2" +if [ -z "$BACKUP_FILE" ]; then + die "Backup file path not provided" +fi + +# Check if backup file already exists +if [ -f "$BACKUP_FILE" ]; then + die "Backup file $BACKUP_FILE already exists" +fi + +# Stop container before backup +_stop_container "$CONTAINER_NAME" + +Create backup of data folder +echo "Creating backup of $LOCAL_DATA_FOLDER..." +if ! tar zcvf "$BACKUP_FILE" -C "$LOCAL_DATA_FOLDER" .; then + _start_container "$CONTAINER_NAME" + die "Failed to create backup" +fi + +# Start container after backup +_start_container "$CONTAINER_NAME" + +echo "Backup created successfully: $BACKUP_FILE"