From d1a739cdd016615ad1086968fa2526165389ba52 Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 2 May 2025 23:00:41 +1200 Subject: [PATCH] Rework backup/restore --- src/main.cpp | 75 +++++++---- src/main_commands.cpp | 280 ----------------------------------------- src/main_commands.hpp | 18 --- src/service_runner.cpp | 225 ++++++++++++++++++++++++++++++--- src/service_runner.hpp | 10 +- 5 files changed, 264 insertions(+), 344 deletions(-) delete mode 100644 src/main_commands.cpp delete mode 100644 src/main_commands.hpp diff --git a/src/main.cpp b/src/main.cpp index d2a4d73..0d2c587 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -8,7 +8,6 @@ #include "utils/utils.hpp" #include "utils/readmes.hpp" #include "autocomplete.hpp" -#include "main_commands.hpp" #include "utils/hash.hpp" #include @@ -69,17 +68,55 @@ int die(const std::string & msg) { return 1; } -bool parseargs(std::string arg2, std::string arg3, std::string & server_name, std::vector& servicelist) +int init(const std::vector &args) +{ + std::string lcd; + + if (args.size() < 3) { + std::cerr << "Error: init command requires a directory argument" << std::endl; + return 1; + } + try { + if (!gConfig().add_local_config_directory(args[2])) + return 1; // error already reported + + gConfig().save_config(); + std::cout << "Config directory added: " << gConfig().get_local_config_directories().back() << std::endl; + dropshell::create_readme_local_config_dir(gConfig().get_local_config_directories().back()); + + if (gConfig().get_local_config_directories().size() ==1) + std::cout << "DropShell is now initialised and you can add a server with 'dropshell create-server '" << std::endl; + else + { + std::cout << "DropShell will now use all of the following directories for configuration:" << std::endl; + for (const auto& dir : gConfig().get_local_config_directories()) { + std::cout << " " << dir << std::endl; + } + std::cout << "You can edit the config file manually with: dropshell edit" << std::endl; + } + } catch (const std::exception& e) { + std::cerr << "Error in init: " << e.what() << std::endl; + return 1; + } + return 0; +} + +struct ServerAndServices { + std::string server_name; + std::vector servicelist; +}; + +bool getCLIServices(const std::string & arg2, const std::string & arg3, + ServerAndServices & server_and_services) { if (arg2.empty()) return false; - server_name = arg2; + server_and_services.server_name = arg2; if (arg3.empty()) { - servicelist = get_server_services_info(server_name); + server_and_services.servicelist = get_server_services_info(arg2); } else { - servicelist.push_back(get_service_info(server_name, arg3)); + server_and_services.servicelist.push_back(get_service_info(arg2, arg3)); } - return true; } @@ -126,7 +163,7 @@ int main(int argc, char* argv[]) { } if (cmd == "init") { - return main_commands::init(argvec); + return init(argvec); } if (cmd == "help" || cmd == "-h" || cmd == "--help" || cmd== "h" || cmd=="halp") { @@ -203,36 +240,28 @@ int main(int argc, char* argv[]) { return 0; } - if (cmd == "backup" || cmd=="backups") { - if (argc < 4) return die("Error: backup requires a target server and target service to back up"); - return main_commands::backup(argvec); - } - - if (cmd == "restore") { - if (argc < 4) return die("Error: restore requires a target server, target service the backup file to restore"); - return main_commands::restore(argvec); - } - // handle running a command. std::set commands; get_all_used_commands(commands); commands.merge(std::set{"ssh","edit","_allservicesstatus"}); // handled by service_runner, but not in template_shell_commands. for (const auto& command : commands) { if (cmd == command) { - std::string server_name; - std::vector servicelist; - if (!parseargs(safearg(argc, argv, 2), safearg(argc, argv, 3), server_name, servicelist)) { + ServerAndServices server_and_services; + if (!getCLIServices(safearg(argc, argv, 2), safearg(argc, argv, 3), server_and_services)) { std::cerr << "Error: " << command << " command requires server name and optionally service name" << std::endl; return 1; } - for (const auto& service_info : servicelist) { - service_runner runner(server_name, service_info.service_name); + for (const auto& service_info : server_and_services.servicelist) { + service_runner runner(server_and_services.server_name, service_info.service_name); if (!runner.isValid()) { std::cerr << "Error: Failed to initialize service" << std::endl; return 1; } - if (!runner.run_command(command)) { + std::vector additional_args; + for (int i=4; i -#include -#include - -namespace dropshell { - -namespace main_commands { - - -static const std::string magic_string = "-_-"; - - -int init(const std::vector &args) -{ - std::string lcd; - - if (args.size() < 3) { - std::cerr << "Error: init command requires a directory argument" << std::endl; - return 1; - } - try { - if (!gConfig().add_local_config_directory(args[2])) - return 1; // error already reported - - gConfig().save_config(); - std::cout << "Config directory added: " << gConfig().get_local_config_directories().back() << std::endl; - dropshell::create_readme_local_config_dir(gConfig().get_local_config_directories().back()); - - if (gConfig().get_local_config_directories().size() ==1) - std::cout << "DropShell is now initialised and you can add a server with 'dropshell create-server '" << std::endl; - else - { - std::cout << "DropShell will now use all of the following directories for configuration:" << std::endl; - for (const auto& dir : gConfig().get_local_config_directories()) { - std::cout << " " << dir << std::endl; - } - std::cout << "You can edit the config file manually with: dropshell edit" << std::endl; - } - } catch (const std::exception& e) { - std::cerr << "Error in init: " << e.what() << std::endl; - return 1; - } - return 0; -} - -int restore(const std::vector &args, bool silent) -{ - if (args.size() < 4) { - std::cerr << "Error: not enough arguments. dropshell restore " << std::endl; - return 1; - } - - std::string server_name = args[2]; - std::string service_name = args[3]; - std::string backup_file = args[4]; - - auto service_info = get_service_info(server_name, service_name); - if (service_info.local_service_path.empty()) { - std::cerr << "Error: Service not found" << std::endl; - return 1; - } - - server_env_manager env(server_name); - if (!env.is_valid()) { - std::cerr << "Error: Invalid server environment" << std::endl; - return 1; - } - - 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 1; - } - - // split the backup filename into parts based on the magic string - std::vector 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 1; - } - - 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 != service_info.template_name) { - std::cerr << "Error: Backup template does not match service template. Can't restore." << std::endl; - return 1; - } - - std::string nicedate = std::string(backup_datetime).substr(0, 10); - - std::cout << "Restoring " << nicedate << " backup of " << backup_template_name << " taken from "<> confirm; - if (confirm != 'y') { - std::cout << "Restore cancelled." << std::endl; - return 1; - } - - // run the restore script - std::cout << "OK, here goes..." << std::endl; - - { // backup existing service - std::cout << "1) Backing up existing service... " << std::flush; - std::vector backup_args = {"dropshell","backup",server_name, service_name}; - if (!backup(backup_args,true)) // silent=true - { - std::cerr << std::endl; - std::cerr << "Error: Backup failed, restore aborted." << std::endl; - std::cerr << "You can try using dropshell install "< /dev/null 2>&1" : ""); - if (!env.execute_local_command(scp_cmd)) { - std::cerr << "Failed to copy backup file from server" << std::endl; - return false; - } - env.run_remote_template_command(service_name, "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= (env.run_remote_template_command(service_name, "status", {}, silent)); - if (!silent) - std::cout << (healthy ? green_tick : red_cross) << " Service is " << (healthy ? "healthy" : "NOT healthy") << std::endl; - - return 0; -} - -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. -// ------------------------------------------------------------------------------------------------ -int backup(const std::vector & args, bool silent) { - if (args.size() < 4) { - std::cerr << "Error: backup command requires a server name and service name" << std::endl; - return 1; - } - - std::string server_name = args[2]; - std::string service_name = args[3]; - - auto service_info = get_service_info(server_name, service_name); - if (service_info.local_service_path.empty()) { - std::cerr << "Error: Service not found" << std::endl; - return 1; - } - - server_env_manager env(server_name); - if (!env.is_valid()) { - std::cerr << "Error: Invalid server environment" << 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(server_name, service_name); - std::string remote_command_script_file = remote_service_template_path + "/" + command + ".sh"; - std::string remote_service_config_path = remotepath::service_config(server_name, service_name); - if (!env.check_remote_items_exist({ - remotepath::service(server_name, service_name), - remote_command_script_file, - remotefile::service_env(server_name, service_name)}) - ) - { - 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(server_name); - if (!silent) std::cout << "Remote backups directory on "<< server_name <<": " << remote_backups_dir << std::endl; - std::string mkdir_cmd = "mkdir -p " + quote(remote_backups_dir); - if (!env.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(server_name)) {std::cerr << "Error: Server name contains invalid character sequence ( -_- ) that would break backup naming scheme" << std::endl; return 1;} - if (name_breaks_backups(service_name)) {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 = server_name + magic_string + service_info.template_name + magic_string + service_name + 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 (!env.run_remote_template_command(service_name, 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 " + env.get_SSH_PORT() + " " + - env.get_SSH_USER() + "@" + env.get_SSH_HOST() + ":" + - quote(remote_backup_file_path) + " " + quote(local_backup_file_path) + (silent ? " > /dev/null 2>&1" : ""); - if (!env.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:"< -#include - -namespace dropshell { - - namespace main_commands { - - int init(const std::vector &args); - int restore(const std::vector &args, bool silent=false); - int backup(const std::vector &args, bool silent=false); - } // namespace main_commands - -} // namespace dropshell - -#endif \ No newline at end of file diff --git a/src/service_runner.cpp b/src/service_runner.cpp index e9af396..93f6a54 100644 --- a/src/service_runner.cpp +++ b/src/service_runner.cpp @@ -18,6 +18,8 @@ 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) { @@ -146,7 +148,7 @@ bool service_runner::uninstall() { // ------------------------------------------------------------------------------------------------ // Run a command on the service. // ------------------------------------------------------------------------------------------------ -bool service_runner::run_command(const std::string& command) { +bool service_runner::run_command(const std::string& command, std::vector additional_args) { if (!mServerEnv.is_valid()) { std::cerr << "Error: Server service not initialized" << std::endl; return false; @@ -195,6 +197,17 @@ bool service_runner::run_command(const std::string& command) { 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 " << std::endl; + return false; + } + return restore(additional_args[0], false); + } + if (command == "backup") { + return backup(false); + } // Run the generic command std::vector args; // not passed through yet. @@ -404,23 +417,6 @@ void edit_file(const std::string &file_path, const std::string & aftertext) exit(EXIT_FAILURE); } -bool service_runner::restore(std::string backup_file) -{ - std::string command = "restore"; - std::string script_path = remotepath::service_template(mServer, mService) + "/" + command + ".sh"; - if (!template_command_exists(mServiceInfo.template_name, command)) { - std::cout << "No restore script for " << mServiceInfo.template_name << std::endl; - return true; // nothing to restore. - } - - /// TOODOOOOOO!!!!!! - std::cout << "Restore not implemented yet" << std::endl; - return true; - - // std::string run_cmd = construct_standard_command_run_cmd("restore"); - // return execute_ssh_command(run_cmd, "Restore script failed"); -} - void service_runner::interactive_ssh_service() { std::set used_commands = get_used_commands(mServer, mService); @@ -446,4 +442,197 @@ void service_runner::edit_service_config() 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 " << 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 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 "<> 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 "< /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:"< ports; } ServiceStatus; + class service_runner { public: service_runner(const std::string& server_name, const std::string& service_name); @@ -44,7 +45,7 @@ class service_runner { // checking that the command exists in the service directory. // checking that the command is a valid .sh file. // checking that the {service_name}.env file exists in the service directory. - bool run_command(const std::string& command); + bool run_command(const std::string& command, std::vector additional_args={}); // check health of service. Silent. // 1. run status.sh on the server @@ -79,10 +80,9 @@ class service_runner { // 4. remove the service directory from the server bool uninstall(); - // restore the service over ssh, using the credentials from server.env (via server_env.hpp) - // 1. copy the backup file to the server's DROPSHELL_DIR/backups folder - // 2. run the restore.sh script on the server, passing the {service_name}.env file as an argument - bool restore(std::string backup_file); + // backup and restore + bool backup(bool silent=false); + bool restore(std::string backup_file, bool silent=false); // launch an interactive ssh session on a server or service // replaces the current dropshell process with the ssh process