531 lines
20 KiB
C++
531 lines
20 KiB
C++
#include "config.hpp"
|
|
#include "service_runner.hpp"
|
|
#include "server_env.hpp"
|
|
#include "templates.hpp"
|
|
#include "services.hpp"
|
|
#include "utils/directories.hpp"
|
|
#include "utils/utils.hpp"
|
|
#include <boost/filesystem.hpp>
|
|
#include <iostream>
|
|
#include <fstream>
|
|
#include <sstream>
|
|
#include <cstdlib>
|
|
#include <chrono>
|
|
#include <iomanip>
|
|
|
|
namespace fs = boost::filesystem;
|
|
|
|
namespace dropshell {
|
|
|
|
|
|
service_runner::service_runner() : m_server_name(""), m_server_env(nullptr) {}
|
|
|
|
bool service_runner::init(const std::string& server_name, const std::string& service_name) {
|
|
if (server_name.empty() || service_name.empty())
|
|
return false;
|
|
|
|
// Initialize server environment
|
|
try {
|
|
m_server_env = std::make_unique<server_env>(server_name);
|
|
if (!m_server_env->is_valid()) {
|
|
std::cerr << "Error: Invalid server environment" << std::endl;
|
|
m_server_env.reset();
|
|
return false;
|
|
}
|
|
} catch (const std::exception& e) {
|
|
std::cerr << "Error: Failed to initialize server environment: " << e.what() << std::endl;
|
|
return false;
|
|
}
|
|
|
|
m_server_name = server_name;
|
|
m_service_info = get_service_info(server_name, service_name);
|
|
|
|
mRemote_service_path = get_remote_service_path(m_server_name, m_service_info.service_name);
|
|
mRemote_service_config_path = get_remote_service_config_path(m_server_name, m_service_info.service_name);
|
|
mRemote_service_template_path = get_remote_service_template_path(m_server_name, m_service_info.service_name);
|
|
mRemote_service_env_file = get_remote_service_env_file(m_server_name, m_service_info.service_name);
|
|
|
|
return !m_service_info.path.empty();
|
|
}
|
|
|
|
// Helper method implementations
|
|
std::string service_runner::construct_ssh_cmd(const server_env &env) {
|
|
std::stringstream ssh_cmd;
|
|
ssh_cmd << "ssh -p " << env.get_SSH_PORT() << " "
|
|
<< env.get_SSH_USER() << "@" << env.get_SSH_HOST() << " ";
|
|
return ssh_cmd.str();
|
|
}
|
|
|
|
std::string service_runner::construct_ssh_cmd() const
|
|
{
|
|
if (!m_server_env)
|
|
return std::string();
|
|
return construct_ssh_cmd(*m_server_env);
|
|
}
|
|
|
|
std::string service_runner::construct_standard_command_run_cmd(const std::string &command) const
|
|
{
|
|
std::string script_path = mRemote_service_template_path + "/" + command + ".sh";
|
|
std::string run_cmd = "'cd " + quote(mRemote_service_template_path) +
|
|
" && /bin/bash "+quote(script_path)+" "+quote(mRemote_service_config_path)+"'";
|
|
return run_cmd;
|
|
}
|
|
|
|
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;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool service_runner::check_remote_file_exists(const std::string& file_path) const {
|
|
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;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool service_runner::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 += fs::path(file_path).filename().string() + " ";
|
|
}
|
|
// check if all items in the vector exist on the remote server, in a single command.
|
|
std::string check_cmd = construct_ssh_cmd() + "'for item in " + file_paths_str + "; do test -f $item; done'";
|
|
if (system(check_cmd.c_str()) != 0) {
|
|
std::cerr << "Error: Required items not found on remote server: " << file_names_str << std::endl;
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool service_runner::execute_ssh_command(const std::string& command, const std::string& error_msg) const {
|
|
std::string full_cmd = construct_ssh_cmd() + command;
|
|
return execute_local_command(full_cmd, error_msg);
|
|
}
|
|
bool service_runner::execute_local_command(const std::string& command, const std::string& error_msg) const {
|
|
bool okay = (system(command.c_str()) == 0);
|
|
|
|
if (!okay && !error_msg.empty())
|
|
std::cerr << "Error: " << error_msg << std::endl;
|
|
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);
|
|
|
|
if (!m_server_env) return false; // should never hit this.
|
|
|
|
// Check if template exists
|
|
template_info tinfo;
|
|
if (!get_template_info(m_service_info.template_name, tinfo))
|
|
return false;
|
|
|
|
// Create service directory
|
|
std::string mkdir_cmd = "'mkdir -p " + quote(mRemote_service_path) + "'";
|
|
if (!execute_ssh_command(mkdir_cmd, "Failed to create service directory"))
|
|
return false;
|
|
|
|
// Check if rsync is installed on remote host
|
|
std::string check_rsync_cmd = "'which rsync > /dev/null 2>&1'";
|
|
if (!execute_ssh_command(check_rsync_cmd, "rsync is not installed on the remote host"))
|
|
return false;
|
|
|
|
// Copy template files
|
|
{
|
|
std::cout << "Copying template files from [LOCAL] " << tinfo.path << std::endl << std::string(24,' ')<<"to [REMOTE] " << mRemote_service_template_path << "/" << std::endl;
|
|
std::string rsync_cmd = "rsync --delete -zrpc -e 'ssh -p " + m_server_env->get_SSH_PORT() + "' " +
|
|
quote(tinfo.path + "/") + " "+
|
|
m_server_env->get_SSH_USER() + "@" + m_server_env->get_SSH_HOST() + ":" +
|
|
quote(mRemote_service_template_path+"/");
|
|
//std::cout << std::endl << rsync_cmd << std::endl << std::endl;
|
|
execute_local_command(rsync_cmd,"Failed to copy template files");
|
|
}
|
|
|
|
// Copy service files (including service.env)
|
|
{
|
|
std::string local_service_path = get_local_service_path(m_server_name, m_service_info.service_name);
|
|
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 service files from [LOCAL] " << local_service_path << std::endl <<std::string(24,' ')<<"to [REMOTE] " << mRemote_service_config_path << std::endl;
|
|
std::string rsync_cmd = "rsync --delete -zrpc -e 'ssh -p " + m_server_env->get_SSH_PORT() + "' " +
|
|
quote(local_service_path + "/") + " "+
|
|
m_server_env->get_SSH_USER() + "@" + m_server_env->get_SSH_HOST() + ":" +
|
|
quote(mRemote_service_config_path + "/");
|
|
execute_local_command(rsync_cmd,"Failed to copy service files");
|
|
}
|
|
|
|
// Run install script
|
|
{
|
|
std::string install_cmd = construct_standard_command_run_cmd("install");
|
|
bool ok= execute_ssh_command(install_cmd, "Failed to run install script");
|
|
if (!ok)
|
|
return false;
|
|
}
|
|
|
|
// print health tick
|
|
std::cout << "Health: " << healthtick() << std::endl;
|
|
return true;
|
|
}
|
|
|
|
bool service_runner::uninstall() {
|
|
maketitle("Uninstalling " + m_service_info.service_name + " (" + m_service_info.template_name + ") on " + m_server_name);
|
|
|
|
if (!m_server_env) return false; // should never hit this.
|
|
|
|
// 2. Check if service directory exists on server
|
|
if (!check_remote_dir_exists(mRemote_service_path)) {
|
|
std::cerr << "Service is not installed: " << m_service_info.service_name << std::endl;
|
|
return true; // Nothing to uninstall
|
|
}
|
|
|
|
// 3. Run uninstall script if it exists
|
|
std::string uninstall_script = mRemote_service_template_path + "/_uninstall.sh";
|
|
bool script_exists = check_remote_file_exists(uninstall_script);
|
|
|
|
if (script_exists) {
|
|
std::string uninstall_cmd = construct_standard_command_run_cmd("uninstall");
|
|
if (!execute_ssh_command(uninstall_cmd, "Failed to run uninstall script")) {
|
|
std::cerr << "Warning: Uninstall script failed, but continuing with directory removal" << std::endl;
|
|
}
|
|
} else {
|
|
std::cerr << "Warning: No uninstall script found. Unable to uninstall service." << std::endl;
|
|
return false;
|
|
}
|
|
|
|
// 4. Remove the service directory from the server
|
|
std::string rm_cmd = "'rm -rf " + quote(mRemote_service_path) + "'";
|
|
if (!execute_ssh_command(rm_cmd, "Failed to remove service directory")) {
|
|
return false;
|
|
}
|
|
|
|
std::cout << "Service " << m_service_info.service_name << " successfully uninstalled from " << m_server_name << std::endl;
|
|
return true;
|
|
}
|
|
|
|
bool service_runner::run_command(const std::string& command) {
|
|
if (!m_server_env) {
|
|
std::cerr << "Error: Server service not initialized" << std::endl;
|
|
return false;
|
|
}
|
|
template_info tinfo;
|
|
if (!get_template_info(m_service_info.template_name, tinfo)) {
|
|
std::cerr << "Error: Template '" << m_service_info.template_name << "' not found" << std::endl;
|
|
return false;
|
|
}
|
|
if (!template_command_exists(m_service_info.template_name, command)) {
|
|
std::cout << "No command script for " << m_service_info.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 = mRemote_service_template_path + "/" + command + ".sh";
|
|
|
|
// Check if service directory exists
|
|
if (!check_remote_dir_exists(mRemote_service_path)) {
|
|
return false;
|
|
}
|
|
|
|
// Check if command script exists
|
|
if (!check_remote_file_exists(script_path)) {
|
|
return false;
|
|
}
|
|
|
|
// Check if env file exists
|
|
if (!check_remote_file_exists(mRemote_service_env_file)) {
|
|
return false;
|
|
}
|
|
|
|
if (command == "uninstall")
|
|
return uninstall();
|
|
if (command == "backup")
|
|
return backup();
|
|
if (command == "ssh") {
|
|
interactive_ssh_service();
|
|
return true;
|
|
}
|
|
|
|
// Run the generic command
|
|
std::string run_cmd = construct_standard_command_run_cmd(command);
|
|
return execute_ssh_command(run_cmd, "Command returned error code: " + script_path);
|
|
}
|
|
|
|
bool service_runner::backup() {
|
|
maketitle("Backing up " + m_service_info.service_name + " (" + m_service_info.template_name + ") on " + m_server_name);
|
|
|
|
if (!m_server_env) return false; // should never hit this.
|
|
|
|
std::string command = "backup";
|
|
|
|
std::string script_path = mRemote_service_template_path + "/" + command + ".sh";
|
|
if (!template_command_exists(m_service_info.template_name, command)) {
|
|
std::cout << "No backup script for " << m_service_info.template_name << std::endl;
|
|
return true; // nothing to back up.
|
|
}
|
|
|
|
// Check if basic installed stuff is in place.
|
|
if (!check_remote_items_exist({mRemote_service_path,script_path,mRemote_service_env_file}))
|
|
{
|
|
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 server_backups_dir = m_server_env->get_DROPSHELL_DIR() + "/backups";
|
|
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;
|
|
}
|
|
|
|
// Create backups directory locally if it doesn't exist
|
|
std::string local_backups_dir = get_local_config_backups_path(0);
|
|
if (local_backups_dir.empty()) {
|
|
std::cerr << "Error: Local backups directory not found - is DropShell initialised?" << std::endl;
|
|
return false;
|
|
}
|
|
if (!fs::exists(local_backups_dir)) {
|
|
fs::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");
|
|
|
|
// Construct backup filename
|
|
std::string backup_filename = m_server_name + "-" + m_service_info.service_name + "-" + datetime.str() + ".tgz";
|
|
std::string server_backup_path = server_backups_dir + "/" + backup_filename;
|
|
std::string local_backup_path = (fs::path(local_backups_dir) / backup_filename).string();
|
|
|
|
// Run backup script
|
|
std::string backup_cmd = "'cd " + quote(mRemote_service_template_path) +
|
|
" && /bin/bash "+quote(script_path)+" "+quote(mRemote_service_config_path)+" "+
|
|
quote(server_backup_path)+"'";
|
|
|
|
if (!execute_ssh_command(backup_cmd, "Backup script failed")) {
|
|
std::cerr << "Backup script failed: " << backup_cmd << std::endl;
|
|
return false;
|
|
}
|
|
|
|
// 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() + ":" +
|
|
quote(server_backup_path) + " " + quote(local_backup_path);
|
|
if (!execute_local_command(scp_cmd, "Failed to copy backup file from server")) {
|
|
return false;
|
|
}
|
|
|
|
std::cout << "Backup created successfully: " << local_backup_path << std::endl;
|
|
return true;
|
|
}
|
|
|
|
std::map<std::string, ServiceStatus> service_runner::get_all_services_status(std::string server_name)
|
|
{
|
|
std::map<std::string, ServiceStatus> 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;
|
|
}
|
|
|
|
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);
|
|
|
|
server_env env(server_name);
|
|
if (!env.is_valid()) {
|
|
std::cerr << "Error: Invalid server environment" << std::endl;
|
|
return status;
|
|
}
|
|
|
|
std::string script_path = remote_service_template_path + "/" + command + ".sh";
|
|
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<std::string> 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 (!m_server_env) {
|
|
std::cerr << "Error: Server service not initialized" << std::endl;
|
|
return HealthStatus::ERROR;
|
|
}
|
|
|
|
// Check if status script exists
|
|
std::string command = "status";
|
|
|
|
if (!template_command_exists(m_service_info.template_name, command)) {
|
|
return HealthStatus::UNKNOWN;
|
|
}
|
|
|
|
std::string script_path = mRemote_service_template_path + "/" + command + ".sh";
|
|
if (!check_remote_file_exists(script_path)) {
|
|
std::cerr << "Service is not installed: " << m_service_info.service_name << std::endl;
|
|
return HealthStatus::NOTINSTALLED;
|
|
}
|
|
|
|
// Run status script, does not display output.
|
|
std::string run_cmd = construct_standard_command_run_cmd("status");
|
|
bool ok = execute_ssh_command(run_cmd, "");
|
|
if (!ok)
|
|
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[33m?\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 ":question:";
|
|
else if (status == HealthStatus::NOTINSTALLED)
|
|
return ":warning:";
|
|
else
|
|
return ":error:";
|
|
}
|
|
|
|
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 = get_local_server_path(server_name);
|
|
if (serverpath.empty()) {
|
|
std::cerr << "Error: Server not found: " << server_name << std::endl;
|
|
return;
|
|
}
|
|
|
|
server_env 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", login.c_str(), "-p", ssh_port.c_str(), nullptr);
|
|
else
|
|
execlp("ssh", "ssh", 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 service_runner::interactive_ssh_service()
|
|
{
|
|
std::set<std::string> used_commands = get_used_commands(m_server_name, m_service_info.service_name);
|
|
if (used_commands.find("ssh") == used_commands.end()) {
|
|
std::cerr << "Error: "<< m_service_info.service_name <<" does not support ssh" << std::endl;
|
|
return;
|
|
}
|
|
|
|
std::string command = construct_standard_command_run_cmd("ssh");
|
|
interactive_ssh(m_server_name, command);
|
|
}
|
|
|
|
|
|
|
|
} // namespace dropshell
|