762 lines
30 KiB
C++
762 lines
30 KiB
C++
#include <iostream>
|
|
#include <fstream>
|
|
#include <sstream>
|
|
#include <cstdlib>
|
|
#include <chrono>
|
|
#include <iomanip>
|
|
#include <filesystem>
|
|
#include <unistd.h>
|
|
#include <libassert/assert.hpp>
|
|
|
|
#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 "utils/runner.hpp"
|
|
|
|
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);
|
|
if (mServiceInfo.service_name.empty())
|
|
return;
|
|
|
|
mService = mServiceInfo.service_name;
|
|
|
|
mValid = !mServiceInfo.local_template_path.empty();
|
|
}
|
|
|
|
bool service_runner::install(bool silent) {
|
|
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 = gTemplateManager().get_template_info(mServiceInfo.template_name);
|
|
if (!tinfo.is_set())
|
|
return false;
|
|
|
|
// Create service directory
|
|
if (0!=runner::execute_cmd("mkdir",{"-p",remotepath::service(mServer, mService)},"",{},true,false,mServerEnv.get_SSH_INFO()))
|
|
{
|
|
std::cerr << "Failed to create service directory " << remotepath::service(mServer, mService) << std::endl;
|
|
return false;
|
|
}
|
|
|
|
// Check if rsync is installed on remote host
|
|
if (0!=runner::execute_cmd("which",{"rsync"},"",{},true,false,mServerEnv.get_SSH_INFO()))
|
|
{
|
|
std::cerr << "rsync is not installed on the remote host" << std::endl;
|
|
return false;
|
|
}
|
|
|
|
// make sure all shell files are executable
|
|
make_shell_files_executable(tinfo.local_template_path().string());
|
|
|
|
// Copy template files
|
|
{
|
|
if (!rsync_copy(tinfo.local_template_path().string()+"/", remotepath::service_template(mServer, mService)+"/", silent)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Copy service files
|
|
{
|
|
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;
|
|
}
|
|
|
|
if (!rsync_copy(local_service_path + "/", remotepath::service_config(mServer,mService) + "/", silent)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Run install script
|
|
{
|
|
mServerEnv.run_remote_template_command(mService, "install", {}, silent, {});
|
|
}
|
|
|
|
// print health tick
|
|
std::cout << "Health: " << healthtick() << std::endl;
|
|
return true;
|
|
}
|
|
|
|
bool service_runner::uninstall(bool silent) {
|
|
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", {}, silent, {})) {
|
|
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
|
|
if (0!=runner::execute_cmd("rm -rf " + quote(remotepath::service(mServer, mService)), {}, "", {}, true, false, mServerEnv.get_SSH_INFO()))
|
|
std::cerr << "Failed to remove remote service directory at " << remotepath::service(mServer, mService) << std::endl;
|
|
|
|
std::cout << "Service " << mService << " successfully uninstalled from " << mServer << std::endl;
|
|
return true;
|
|
}
|
|
|
|
bool service_runner::nuke(bool silent)
|
|
{
|
|
maketitle("Nuking " + mService + " (" + mServiceInfo.template_name + ") on " + mServer);
|
|
|
|
if (!mServerEnv.is_valid()) return false; // should never hit this.
|
|
|
|
std::string remote_service_path = remotepath::service(mServer, mService);
|
|
bool okay = mServerEnv.run_remote_template_command("dropshell-agent", "_nuke_other", {mService, remote_service_path}, silent, {});
|
|
if (!okay)
|
|
{
|
|
std::cerr << "Warning: Nuke script failed" << std::endl;
|
|
return false;
|
|
}
|
|
|
|
std::cout << "Service " << mService << " successfully nuked from " << mServer << std::endl;
|
|
|
|
if (!silent) {
|
|
std::cout << "There's nothing left on the remote server." << std::endl;
|
|
std::cout << "You can remove the local files with:" << std::endl;
|
|
std::cout << " rm -rf " << localpath::service(mServer,mService) << std::endl;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool service_runner::fullnuke()
|
|
{
|
|
if (!nuke(true))
|
|
{
|
|
std::cerr << "Warning: Nuke script failed, aborting fullnuke!" << std::endl;
|
|
return false;
|
|
}
|
|
|
|
std::string local_service_path = mServiceInfo.local_service_path;
|
|
if (local_service_path.empty() || !fs::exists(local_service_path)) {
|
|
std::cerr << "Error: Service directory not found: " << local_service_path << std::endl;
|
|
return false;
|
|
}
|
|
|
|
if (0!=runner::execute_cmd("rm -rf " + quote(local_service_path), {}, "", {}, true, false, mServerEnv.get_SSH_INFO()))
|
|
std::cerr << "Failed to remove local service directory at " << local_service_path << std::endl;
|
|
|
|
std::cout << "Service " << mService << " successfully fully nuked 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, std::map<std::string, std::string> env_vars) {
|
|
if (!mServerEnv.is_valid()) {
|
|
std::cerr << "Error: Server service not initialized" << std::endl;
|
|
return false;
|
|
}
|
|
template_info tinfo = gTemplateManager().get_template_info(mServiceInfo.template_name);
|
|
if (!tinfo.is_set()) {
|
|
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 (command == "fullnuke")
|
|
return fullnuke();
|
|
|
|
if (command == "nuke")
|
|
{
|
|
std::cout << "Nuking " << mService << " (" << mServiceInfo.template_name << ") on " << mServer << std::endl;
|
|
return nuke();
|
|
}
|
|
|
|
if (!gTemplateManager().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))) {
|
|
std::cerr << "Error: Service is not installed: " << mService << std::endl;
|
|
return false;
|
|
}
|
|
|
|
// Check if command script exists
|
|
if (!mServerEnv.check_remote_file_exists(script_path)) {
|
|
std::cerr << "Error: Remote command script not found: " << script_path << std::endl;
|
|
return false;
|
|
}
|
|
|
|
// Check if env file exists
|
|
if (!mServerEnv.check_remote_file_exists(remotefile::service_env(mServer, mService))) {
|
|
std::cerr << "Error: Service config file not found: " << remotefile::service_env(mServer, mService) << std::endl;
|
|
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, false, env_vars);
|
|
}
|
|
|
|
|
|
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";
|
|
|
|
|
|
server_env_manager env(server_name);
|
|
if (!env.is_valid()) {
|
|
std::cerr << "Error: Invalid server environment" << std::endl;
|
|
return status;
|
|
}
|
|
|
|
std::string cmd_path = remotepath::service_template(server_name,service_name) + "/shared/";
|
|
std::string output;
|
|
if (0!=runner::execute_cmd("bash",{cmd_path+command+".sh"}, cmd_path, {}, true, false, env.get_SSH_INFO(), &output))
|
|
{
|
|
std::cerr << "Error: Failed to run command script at " << cmd_path << std::endl;
|
|
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:";
|
|
}
|
|
|
|
|
|
std::string service_runner::healthmark()
|
|
{
|
|
HealthStatus status = is_healthy();
|
|
return HealthStatus2String(status);
|
|
}
|
|
|
|
bool service_runner::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 false;
|
|
}
|
|
|
|
server_env_manager env(server_name);
|
|
if (!env.is_valid()) {
|
|
std::cerr << "Error: Invalid server environment file: " << server_name << std::endl;
|
|
return false;
|
|
}
|
|
return 0==runner::execute_cmd("", {}, "", {}, false, true, env.get_SSH_INFO());
|
|
}
|
|
|
|
void service_runner::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";
|
|
if (!edit_file(config_file)) {
|
|
std::cerr << "Error: Failed to edit server.env" << std::endl;
|
|
std::cerr << "You can manually edit this file at: " << config_file << std::endl;
|
|
std::cerr << "After editing, " << aftertext.str() << std::endl;
|
|
}
|
|
else
|
|
std::cout << aftertext.str() << std::endl;
|
|
}
|
|
|
|
bool service_runner::edit_file(const std::string &file_path)
|
|
{
|
|
// make sure parent directory exists.
|
|
std::string parent_dir = get_parent(file_path);
|
|
std::filesystem::create_directories(parent_dir);
|
|
|
|
std::string editor_cmd;
|
|
const char* editor_env = std::getenv("EDITOR");
|
|
|
|
if (editor_env && std::strlen(editor_env) > 0) {
|
|
editor_cmd = std::string(editor_env);
|
|
} else if (isatty(STDIN_FILENO)) {
|
|
// Check if stdin is connected to a terminal if EDITOR is not set
|
|
editor_cmd = "nano";
|
|
} else {
|
|
std::cerr << "Error: Standard input is not a terminal and EDITOR environment variable is not set." << std::endl;
|
|
std::cerr << "Try setting the EDITOR environment variable (e.g., export EDITOR=nano) or run in an interactive terminal." << std::endl;
|
|
std::cerr << "You can manually edit the file at: " << file_path << std::endl;
|
|
return false;
|
|
}
|
|
|
|
std::cout << "Editing file: " << file_path << std::endl;
|
|
return 0==runner::execute_cmd(editor_cmd, {file_path}, "", {}, false, true, nullptr);
|
|
}
|
|
|
|
bool 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 false;
|
|
}
|
|
|
|
std::vector<std::string> args; // not passed through yet.
|
|
return mServerEnv.run_remote_template_command(mService, "ssh", args, false, {});
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
if (edit_file(config_file) && std::filesystem::exists(config_file))
|
|
std::cout << "To apply your changes, run:\n dropshell install " + mServer + " " + mService << std::endl;
|
|
}
|
|
|
|
|
|
|
|
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 = gConfig().get_local_backup_path();
|
|
|
|
if (backup_file == "latest") {
|
|
// get the latest backup file from the server
|
|
backup_file = get_latest_backup_file(mServer, mService);
|
|
}
|
|
|
|
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;
|
|
|
|
// run the restore script
|
|
std::cout << "OK, here goes..." << std::endl;
|
|
|
|
{ // backup existing service
|
|
maketitle("1) Backing up old service... ");
|
|
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;
|
|
}
|
|
|
|
{ // uninstall service, then nuke it.
|
|
maketitle("2) Uninstalling old service...");
|
|
if (!uninstall(true))
|
|
return false;
|
|
|
|
maketitle("3) Nuking old service...");
|
|
if (!nuke(true))
|
|
return false;
|
|
}
|
|
|
|
|
|
{ // restore service from backup
|
|
maketitle("4) Restoring service data from backup...");
|
|
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
|
|
if (0!=runner::execute_cmd("scp", {quote(local_backup_file_path), mServerEnv.get_SSH_USER() + "@" + mServerEnv.get_SSH_HOST() + ":" + quote(remote_backup_file_path)}, "", {}, false, false, mServerEnv.get_SSH_INFO()))
|
|
{
|
|
std::cerr << "Failed to copy backup file from server" << std::endl;
|
|
return false;
|
|
}
|
|
|
|
cRemoteTempFolder remote_temp_folder(mServerEnv);
|
|
mServerEnv.run_remote_template_command(mService, "restore", {}, silent, {{"BACKUP_FILE", remote_backup_file_path}, {"TEMP_DIR", remote_temp_folder.path()}});
|
|
} // dtor of remote_temp_folder will clean up the temp folder on the server
|
|
|
|
|
|
{ // installing fresh service
|
|
maketitle("5) Non-destructive install of fresh service...");
|
|
if (!install(true))
|
|
return false;
|
|
}
|
|
|
|
bool healthy = false;
|
|
{// healthcheck the service
|
|
maketitle("6) Healthchecking service...");
|
|
std::string green_tick = "\033[32m✓\033[0m";
|
|
std::string red_cross = "\033[31m✗\033[0m";
|
|
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 healthy;
|
|
}
|
|
|
|
|
|
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 (!gTemplateManager().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;
|
|
if (0!=runner::execute_cmd("mkdir", {"-p",quote(remote_backups_dir)}, "", {}, true, false, mServerEnv.get_SSH_INFO()))
|
|
{
|
|
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 = gConfig().get_local_backup_path();
|
|
if (local_backups_dir.empty()) {
|
|
std::cerr << "Error: Local backups directory not found" << std::endl;
|
|
std::cerr << "Run 'dropshell edit' to configure DropShell" << 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
|
|
cRemoteTempFolder remote_temp_folder(mServerEnv);
|
|
if (!mServerEnv.run_remote_template_command(mService, command, {}, silent, {{"BACKUP_FILE", remote_backup_file_path}, {"TEMP_DIR", remote_temp_folder.path()}})) {
|
|
std::cerr << "Backup script failed on remote server: " << remote_backup_file_path << std::endl;
|
|
return false;
|
|
}
|
|
|
|
// Copy backup file from server to local
|
|
if (0!=runner::execute_cmd("scp", {quote(remote_backup_file_path), quote(local_backup_file_path)}, "", {}, silent, false, mServerEnv.get_SSH_INFO()))
|
|
{
|
|
std::cerr << "Failed to copy backup file from server" << std::endl;
|
|
return false;
|
|
}
|
|
} // dtor of remote_temp_folder will clean up the temp folder on the server
|
|
|
|
if (!silent) {
|
|
std::cout << "Backup created successfully. Restore with:"<<std::endl;
|
|
std::cout << " dropshell restore " << mServer << " " << mService << " " << backup_filename << std::endl;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
cRemoteTempFolder::cRemoteTempFolder(const server_env_manager &server_env) : mServerEnv(server_env)
|
|
{
|
|
std::string p = remotepath::temp_files(server_env.get_server_name()) + "/" + random_alphanumeric_string(10);
|
|
if (0!=runner::execute_cmd("mkdir", {"-p",quote(p)}, "", {}, true, false, server_env.get_SSH_INFO()))
|
|
std::cerr << "Failed to create temp directory on server" << std::endl;
|
|
else
|
|
mPath = p;
|
|
}
|
|
|
|
cRemoteTempFolder::~cRemoteTempFolder()
|
|
{
|
|
if (0!=runner::execute_cmd("rm", {"-rf",quote(mPath)}, "", {}, true, false, mServerEnv.get_SSH_INFO()))
|
|
std::cerr << "Failed to remove temp directory on server" << std::endl;
|
|
}
|
|
|
|
std::string cRemoteTempFolder::path() const
|
|
{
|
|
return mPath;
|
|
}
|
|
|
|
// Helper function to get the latest backup file for a given server and service
|
|
std::string service_runner::get_latest_backup_file(const std::string& server, const std::string& service) {
|
|
std::string local_backups_dir = gConfig().get_local_backup_path();
|
|
if (local_backups_dir.empty() || !std::filesystem::exists(local_backups_dir)) {
|
|
std::cerr << "Error: Local backups directory not found: " << local_backups_dir << std::endl;
|
|
return "";
|
|
}
|
|
|
|
// Get the template name for this service
|
|
LocalServiceInfo info = get_service_info(server, service);
|
|
if (info.template_name.empty()) {
|
|
std::cerr << "Error: Could not determine template name for service: " << service << std::endl;
|
|
return "";
|
|
}
|
|
|
|
// Build the expected prefix for backup files
|
|
std::string prefix = server + magic_string + info.template_name + magic_string + service + magic_string;
|
|
std::string latest_file;
|
|
std::string latest_datetime;
|
|
|
|
std::cout << "Looking for backup files in " << local_backups_dir << std::endl;
|
|
|
|
for (const auto& entry : std::filesystem::directory_iterator(local_backups_dir)) {
|
|
if (!entry.is_regular_file()) continue;
|
|
std::string filename = entry.path().filename().string();
|
|
if (filename.rfind(prefix, 0) == 0) { // starts with prefix
|
|
// Extract the datetime part
|
|
size_t dt_start = prefix.size();
|
|
size_t dt_end = filename.find(".tgz", dt_start);
|
|
if (dt_end == std::string::npos) continue;
|
|
std::string datetime = filename.substr(dt_start, dt_end - dt_start);
|
|
std::cout << "Found backup file: " << filename << " with datetime: " << datetime << std::endl;
|
|
if (datetime > latest_datetime) {
|
|
latest_datetime = datetime;
|
|
latest_file = filename;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (latest_file.empty()) {
|
|
std::cerr << "Error: No backup files found for " << server << ", " << service << std::endl;
|
|
}
|
|
|
|
std::cout << "Latest backup file: " << latest_file << std::endl;
|
|
return latest_file;
|
|
}
|
|
|
|
bool service_runner::rsync_copy(const std::string& local_path, const std::string& remote_path, bool silent) {
|
|
std::cout << "Copying: [LOCAL] " << local_path << std::endl << std::string(8,' ')<<"[REMOTE] " << remote_path << std::endl;
|
|
|
|
if (0 != runner::execute_cmd(
|
|
"rsync",
|
|
{"--delete","--mkpath","-zrpc","-e",quote("ssh -p " + mServerEnv.get_SSH_PORT()),local_path,
|
|
mServerEnv.get_SSH_USER()+"@"+mServerEnv.get_SSH_HOST()+":"+remote_path},
|
|
"", {}, true, false)) {
|
|
std::cerr << "Failed to copy files using rsync" << std::endl;
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
} // namespace dropshell
|