dropshell/src/service_runner.cpp
2025-05-02 23:00:41 +12:00

638 lines
25 KiB
C++

#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 <iostream>
#include <fstream>
#include <sstream>
#include <cstdlib>
#include <chrono>
#include <iomanip>
#include <filesystem>
#include <unistd.h>
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 <<std::string(8,' ')<<"[REMOTE] " << remotepath::service_config(mServer,mService) << std::endl;
std::string rsync_cmd = "rsync --delete -zrpc -e 'ssh -p " + mServerEnv.get_SSH_PORT() + "' " +
quote(local_service_path + "/") + " "+
mServerEnv.get_SSH_USER() + "@" + mServerEnv.get_SSH_HOST() + ":" +
quote(remotepath::service_config(mServer,mService) + "/");
if (!mServerEnv.execute_local_command(rsync_cmd))
{
std::cerr << "Failed to copy service files using rsync" << std::endl;
return false;
}
}
// Run install script
{
mServerEnv.run_remote_template_command(mService, "install", {});
}
// print health tick
std::cout << "Health: " << healthtick() << std::endl;
return true;
}
bool service_runner::uninstall() {
maketitle("Uninstalling " + mService + " (" + mServiceInfo.template_name + ") on " + mServer);
if (!mServerEnv.is_valid()) return false; // should never hit this.
// 2. Check if service directory exists on server
if (!mServerEnv.check_remote_dir_exists(remotepath::service(mServer, mService))) {
std::cerr << "Service is not installed: " << mService << std::endl;
return true; // Nothing to uninstall
}
// 3. Run uninstall script if it exists
std::string uninstall_script = remotepath::service_template(mServer, mService) + "/_uninstall.sh";
bool script_exists = mServerEnv.check_remote_file_exists(uninstall_script);
if (script_exists) {
if (!mServerEnv.run_remote_template_command(mService, "uninstall", {})) {
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(remotepath::service(mServer, mService)) + "'";
if (!mServerEnv.execute_ssh_command(rm_cmd)) {
std::cerr << "Failed to remove service directory" << std::endl;
return false;
}
std::cout << "Service " << mService << " successfully uninstalled from " << mServer << std::endl;
return true;
}
// ------------------------------------------------------------------------------------------------
// Run a command on the service.
// ------------------------------------------------------------------------------------------------
bool service_runner::run_command(const std::string& command, std::vector<std::string> 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 <server> <service> <backup-file>" << std::endl;
return false;
}
return restore(additional_args[0], false);
}
if (command == "backup") {
return backup(false);
}
// Run the generic command
std::vector<std::string> args; // not passed through yet.
return mServerEnv.run_remote_template_command(mService, command, args);
}
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;
}
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<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 (!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 "<<server_name<<"\n"
<< "Once moved, reinstall all services with: dropshell install " << server_name;
std::string config_file = serverpath + "/server.env";
edit_file(config_file, aftertext.str());
}
void edit_file(const std::string &file_path, const std::string & aftertext)
{
std::string cmd = "nano -w "+file_path+" ; echo \""+aftertext+"\"";
execlp("bash","bash","-c",cmd.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(mServer, mService);
if (used_commands.find("ssh") == used_commands.end()) {
std::cerr << "Error: "<< mService <<" does not support ssh" << std::endl;
return;
}
std::vector<std::string> 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 <server> <service> <backup-file>" << 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<std::string> 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 "<<backup_server_name<<", onto "<<mServer<<"/"<<mService<<std::endl;
std::cout << std::endl;
std::cout << "*** ALL DATA FOR "<<mServer<<"/"<<mService<<" WILL BE OVERWRITTEN! ***"<<std::endl;
std::cout << std::endl;
std::cout << "Are you sure you want to continue? (y/n)" << std::endl;
char confirm;
std::cin >> 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 "<<mServer<<" "<<mService<<" to install the service afresh." << std::endl;
std::cerr << "Otherwise, stop the service, create and initialise a new one, then restore to that." << std::endl;
return false;
}
std::cout << "Backup complete." << std::endl;
}
{ // restore service from backup
std::cout << "2) Restoring service from backup..." << std::endl;
std::string remote_backups_dir = remotepath::backups(mServer);
std::string remote_backup_file_path = remote_backups_dir + "/" + backup_file;
// Copy backup file from local to server
std::string scp_cmd = "scp -P " + mServerEnv.get_SSH_PORT() + " " + quote(local_backup_file_path) + " " + mServerEnv.get_SSH_USER() + "@" + mServerEnv.get_SSH_HOST() + ":" + quote(remote_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;
}
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:"<<std::endl;
std::cout << " dropshell restore " << mServer << " " << mService << " " << backup_filename << std::endl;
}
return true;
}
} // namespace dropshell