diff --git a/source/src/commands/backupdata.cpp b/source/src/commands/backupdata.cpp index b801dba..189d36d 100644 --- a/source/src/commands/backupdata.cpp +++ b/source/src/commands/backupdata.cpp @@ -16,157 +16,159 @@ #include "utils/directories.hpp" #include "shared_commands.hpp" +namespace dropshell +{ -namespace dropshell { + int backupdata_handler(const CommandContext &ctx); -int backupdata_handler(const CommandContext& ctx); + static std::vector backupdata_name_list = {"backupdata", "bd", "backup", "bup"}; -static std::vector backupdata_name_list={"backupdata","bd","backup","bup"}; - -// Static registration -struct BackupDataCommandRegister { - BackupDataCommandRegister() { - CommandRegistry::instance().register_command({ - backupdata_name_list, - backupdata_handler, - shared_commands::std_autocomplete_allowall, - false, // hidden - true, // requires_config - true, // requires_install - 2, // min_args (after command) - 2, // max_args (after command) - "backupdata SERVER SERVICE", - "Backup data for a service on a server.", - // heredoc - R"( + // Static registration + struct BackupDataCommandRegister + { + BackupDataCommandRegister() + { + CommandRegistry::instance().register_command({backupdata_name_list, + backupdata_handler, + shared_commands::std_autocomplete_allowall, + false, // hidden + true, // requires_config + true, // requires_install + 2, // min_args (after command) + 2, // max_args (after command) + "backupdata SERVER SERVICE", + "Backup data for a service on a server.", + // heredoc + R"( backupdata SERVER SERVICE Backup data for a service on a server. backupdata SERVER all Backup data for all services on a server. Note: This command will not create any data or configuration. It will simply backup the data on the remote server, saving it to a local file. Restore the data with restore. - )" - }); - } -} backupdata_command_register; + )"}); + } + } backupdata_command_register; - -bool backupdata_service(const std::string& server, const std::string& service) -{ - server_env_manager server_env(server); - if (!server_env.is_valid()) + namespace shared_commands { - error << "Server " << server << " is not valid" << std::endl; - return false; - } - LocalServiceInfo sinfo = get_service_info(server, service); - if (!SIvalid(sinfo)) + bool backupdata_service(const std::string &server, const std::string &service) + { + server_env_manager server_env(server); + if (!server_env.is_valid()) + { + error << "Server " << server << " is not valid" << std::endl; + return false; + } + + LocalServiceInfo sinfo = get_service_info(server, service); + if (!SIvalid(sinfo)) + { + error << "Service " << service << " is not valid" << std::endl; + return false; + } + + const std::string command = "backup"; + + if (!gTemplateManager().template_command_exists(sinfo.template_name, command)) + { + info << service << " has no data to backup" << std::endl; + debug << "(no backup script for " << sinfo.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, service); + std::string remote_command_script_file = remote_service_template_path + "/" + command + ".sh"; + std::string remote_service_config_path = remotepath::service_config(server, service); + if (!server_env.check_remote_items_exist({remotepath::service(server, service), + remote_command_script_file, + remotefile::service_env(server, service)})) + { + error << "Error: Required service directories not found on remote server" << std::endl; + info << "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); + debug << "Remote backups directory on " << server << ": " << remote_backups_dir << std::endl; + std::string mkdir_cmd = "mkdir -p " + quote(remote_backups_dir); + if (!execute_ssh_command(server_env.get_SSH_INFO(), sCommand("", mkdir_cmd, {}), cMode::Defaults)) + { + error << "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()) + { + error << "Error: Local backups directory not found" << std::endl; + info << "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 + shared_commands::cBackupFileName backup_filename_construction(server, service, sinfo.template_name); + if (!backup_filename_construction.is_valid()) + { + error << "Invalid backup filename" << std::endl; + return false; + } + + // Construct backup filename + std::string backup_filename = backup_filename_construction.get_filename(); + 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), "Invalid backup filename"); + + { // Run backup script + shared_commands::cRemoteTempFolder remote_temp_folder(server_env); + if (!server_env.run_remote_template_command(service, command, {}, false, {{"BACKUP_FILE", remote_backup_file_path}, {"TEMP_DIR", remote_temp_folder.path()}})) + { + error << "Backup script failed on remote server: " << remote_backup_file_path << std::endl; + return false; + } + + // Copy backup file from server to local + if (!shared_commands::scp_file_from_remote(server_env, remote_backup_file_path, local_backup_file_path, false)) + { + error << "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 + + info << "Backup created successfully. Restore with:" << std::endl; + info << " dropshell restore " << server << " " << service << " " << backup_filename << std::endl; + return true; + } + } // namespace shared_commands + + int backupdata_handler(const CommandContext &ctx) { - error << "Service " << service << " is not valid" << std::endl; - return false; - } + ASSERT(ctx.args.size() == 2, "Invalid number of arguments"); - const std::string command = "backup"; + std::string server = safearg(ctx.args, 0); + std::string service = safearg(ctx.args, 1); - if (!gTemplateManager().template_command_exists(sinfo.template_name, command)) { - info << service << " has no data to backup" << std::endl; - debug << "(no backup script for " << sinfo.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, service); - std::string remote_command_script_file = remote_service_template_path + "/" + command + ".sh"; - std::string remote_service_config_path = remotepath::service_config(server, service); - if (!server_env.check_remote_items_exist({ - remotepath::service(server, service), - remote_command_script_file, - remotefile::service_env(server, service)}) - ) - { - error << "Error: Required service directories not found on remote server" << std::endl; - info << "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); - debug << "Remote backups directory on "<< server <<": " << remote_backups_dir << std::endl; - std::string mkdir_cmd = "mkdir -p " + quote(remote_backups_dir); - if (!execute_ssh_command(server_env.get_SSH_INFO(), sCommand("",mkdir_cmd, {}), cMode::Defaults)) { - error << "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()) { - error << "Error: Local backups directory not found" << std::endl; - info << "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 - shared_commands::cBackupFileName backup_filename_construction(server, service, sinfo.template_name); - if (!backup_filename_construction.is_valid()) { - error << "Invalid backup filename" << std::endl; - return false; - } - - // Construct backup filename - std::string backup_filename = backup_filename_construction.get_filename(); - 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), "Invalid backup filename"); - - { // Run backup script - shared_commands::cRemoteTempFolder remote_temp_folder(server_env); - if (!server_env.run_remote_template_command(service, command, {}, false, {{"BACKUP_FILE", remote_backup_file_path}, {"TEMP_DIR", remote_temp_folder.path()}})) { - error << "Backup script failed on remote server: " << remote_backup_file_path << std::endl; - return false; + if (service == "all") + { + // backup all services on the server + maketitle("Backing up data for all services on " + server); + bool okay = true; + std::vector services = get_server_services_info(server); + for (const LocalServiceInfo &si : services) + okay &= shared_commands::backupdata_service(server, si.service_name); + return okay ? 0 : 1; } - // Copy backup file from server to local - if (!shared_commands::scp_file_from_remote(server_env, remote_backup_file_path, local_backup_file_path, false)) { - error << "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 - - info << "Backup created successfully. Restore with:"< services = get_server_services_info(server); - for (const auto &service : services) - okay &= backupdata_service(server, service.service_name); - return okay ? 0 : 1; - } - - std::string service = safearg(ctx.args, 1); - return backupdata_service(server, service); -} - } // namespace dropshell \ No newline at end of file diff --git a/source/src/commands/create-service.cpp b/source/src/commands/create-service.cpp index 35ddf66..66b4033 100644 --- a/source/src/commands/create-service.cpp +++ b/source/src/commands/create-service.cpp @@ -11,7 +11,7 @@ namespace dropshell { int create_service_handler(const CommandContext &ctx); - bool create_service(const std::string &server_name, const std::string &template_name, const std::string &service_name, bool silent); + bool create_service(const std::string &server_name, const std::string &template_name, const std::string &service_name); void create_service_autocomplete(const CommandContext &ctx); static std::vector create_service_name_list = {"create-service"}; @@ -45,7 +45,7 @@ namespace dropshell std::string service = safearg(ctx.args, 1); std::string template_name = safearg(ctx.args, 2); - return create_service(server, template_name, service, false) ? 0 : 1; + return create_service(server, template_name, service) ? 0 : 1; } void create_service_autocomplete(const CommandContext &ctx) @@ -58,12 +58,12 @@ namespace dropshell { std::set templates = gTemplateManager().get_template_list(); for (const auto &template_name : templates) - std::cout << template_name << std::endl; + rawout << template_name << std::endl; } } } - bool create_service(const std::string &server_name, const std::string &template_name, const std::string &service_name, bool silent) + bool create_service(const std::string &server_name, const std::string &template_name, const std::string &service_name) { if (server_name.empty() || template_name.empty() || service_name.empty()) return false; @@ -72,44 +72,34 @@ namespace dropshell if (service_dir.empty()) { - if (!silent) - { - std::cerr << "Error: Couldn't locate server " << server_name << " in any config directory" << std::endl; - std::cerr << "Please check the server name is correct and try again" << std::endl; - std::cerr << "You can list all servers with 'dropshell servers'" << std::endl; - std::cerr << "You can create a new server with 'dropshell create-server " << server_name << "'" << std::endl; - } + error << "Couldn't locate server " << server_name << " in any config directory" << std::endl; + info << "Please check the server name is correct and try again" << std::endl; + info << "You can list all servers with 'dropshell servers'" << std::endl; + info << "You can create a new server with 'dropshell create-server " << server_name << "'" << std::endl; return false; } if (std::filesystem::exists(service_dir)) { - if (!silent) - { - std::cerr << "Error: Service already exists: " << service_name << std::endl; - std::cerr << "Current service path: " << service_dir << std::endl; - } + error << "Service already exists: " << service_name << std::endl; + debug << "Current service path: " << service_dir << std::endl; return false; } template_info tinfo = gTemplateManager().get_template_info(template_name); if (!tinfo.is_set()) { - if (!silent) - { - std::cerr << "Error: Template '" << template_name << "' not found" << std::endl; - std::cerr << "Please check the template name is correct and try again" << std::endl; - std::cerr << "You can list all templates with 'dropshell templates'" << std::endl; - std::cerr << "You can create a new template with 'dropshell create-template " << template_name << "'" << std::endl; - } + error << "Template '" << template_name << "' not found" << std::endl; + info << "Please check the template name is correct and try again" << std::endl; + info << "You can list all templates with 'dropshell templates'" << std::endl; + info << "You can create a new template with 'dropshell create-template " << template_name << "'" << std::endl; return false; } // check template is all good. if (!gTemplateManager().test_template(tinfo.local_template_path())) { - if (!silent) - std::cerr << "Error: Template '" << template_name << "' is not valid" << std::endl; + error << "Template '" << template_name << "' is not valid" << std::endl; return false; } @@ -119,14 +109,11 @@ namespace dropshell // copy the template config files to the service directory recursive_copy(tinfo.local_template_path() / "config", service_dir); - if (!silent) - { - std::cout << "Service " << service_name << " created successfully" << std::endl; - std::cout << std::endl; - std::cout << "To complete the installation, please:" << std::endl; - std::cout << "1. edit the service config file: dropshell edit " << server_name << " " << service_name << std::endl; - std::cout << "2. install the remote service: dropshell install " << server_name << " " << service_name << std::endl; - } + info << "Service " << service_name << " created successfully" << std::endl; + info << std::endl; + info << "To complete the installation, please:" << std::endl; + info << "1. edit the service config file: dropshell edit " << server_name << " " << service_name << std::endl; + info << "2. install the remote service: dropshell install " << server_name << " " << service_name << std::endl; return true; } diff --git a/source/src/commands/install.cpp b/source/src/commands/install.cpp index 093a9f0..05e1c59 100644 --- a/source/src/commands/install.cpp +++ b/source/src/commands/install.cpp @@ -53,84 +53,87 @@ namespace dropshell } } install_command_register; - - // ------------------------------------------------------------------------------------------------ - // install service over ssh : SHARED COMMAND - // ------------------------------------------------------------------------------------------------ - bool install_service(const std::string &server, const std::string &service, bool silent) + namespace shared_commands { - LocalServiceInfo service_info = get_service_info(server, service); - if (!SIvalid(service_info)) - return false; - server_env_manager server_env(server); - if (!server_env.is_valid()) - return false; - - maketitle("Installing " + service + " (" + service_info.template_name + ") on " + server); - - if (!server_env.is_valid()) - return false; // should never hit this. - - // Check if template exists - template_info tinfo = gTemplateManager().get_template_info(service_info.template_name); - if (!tinfo.is_set()) - return false; - - if (!tinfo.template_valid()) + // ------------------------------------------------------------------------------------------------ + // install service over ssh : SHARED COMMAND + // ------------------------------------------------------------------------------------------------ + bool install_service(const std::string &server, const std::string &service) { - std::cerr << "Template is not valid: " << service_info.template_name << std::endl; - return false; - } + LocalServiceInfo service_info = get_service_info(server, service); + if (!SIvalid(service_info)) + return false; - // Create service directory - std::string remote_service_path = remotepath::service(server, service); - std::string mkdir_cmd = "mkdir -p " + quote(remote_service_path); - if (!execute_ssh_command(server_env.get_SSH_INFO(), sCommand("", mkdir_cmd, {}), cMode::Silent)) - { - std::cerr << "Failed to create service directory " << remote_service_path << std::endl; - return false; - } + server_env_manager server_env(server); + if (!server_env.is_valid()) + return false; - // Check if rsync is installed on remote host - std::string check_rsync_cmd = "which rsync"; - if (!execute_ssh_command(server_env.get_SSH_INFO(), sCommand("", check_rsync_cmd, {}), cMode::Silent)) - { - std::cerr << "rsync is not installed on the remote host" << std::endl; - return false; - } + maketitle("Installing " + service + " (" + service_info.template_name + ") on " + server); - // Copy template files - debug << "Copying: [LOCAL] " << tinfo.local_template_path() << std::endl + if (!server_env.is_valid()) + return false; // should never hit this. + + // Check if template exists + template_info tinfo = gTemplateManager().get_template_info(service_info.template_name); + if (!tinfo.is_set()) + return false; + + if (!tinfo.template_valid()) + { + std::cerr << "Template is not valid: " << service_info.template_name << std::endl; + return false; + } + + // Create service directory + std::string remote_service_path = remotepath::service(server, service); + std::string mkdir_cmd = "mkdir -p " + quote(remote_service_path); + if (!execute_ssh_command(server_env.get_SSH_INFO(), sCommand("", mkdir_cmd, {}), cMode::Silent)) + { + 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"; + if (!execute_ssh_command(server_env.get_SSH_INFO(), sCommand("", check_rsync_cmd, {}), cMode::Silent)) + { + std::cerr << "rsync is not installed on the remote host" << std::endl; + return false; + } + + // Copy template files + debug << "Copying: [LOCAL] " << tinfo.local_template_path() << std::endl << std::string(8, ' ') << "[REMOTE] " << remotepath::service_template(server, service) << "/" << std::endl; - if (!shared_commands::rsync_tree_to_remote(tinfo.local_template_path().string(), remotepath::service_template(server, service), - server_env, silent)) - { - std::cerr << "Failed to copy template files using rsync" << std::endl; - return false; - } + if (!shared_commands::rsync_tree_to_remote(tinfo.local_template_path().string(), remotepath::service_template(server, service), + server_env, false)) + { + std::cerr << "Failed to copy template files using rsync" << std::endl; + return false; + } - // Copy service files - debug << "Copying: [LOCAL] " << localpath::service(server, service) << std::endl + // Copy service files + debug << "Copying: [LOCAL] " << localpath::service(server, service) << std::endl << std::string(8, ' ') << "[REMOTE] " << remotepath::service_config(server, service) << std::endl; - if (!shared_commands::rsync_tree_to_remote(localpath::service(server, service), remotepath::service_config(server, service), - server_env, silent)) - { - std::cerr << "Failed to copy service files using rsync" << std::endl; - return false; + if (!shared_commands::rsync_tree_to_remote(localpath::service(server, service), remotepath::service_config(server, service), + server_env, false)) + { + std::cerr << "Failed to copy service files using rsync" << std::endl; + return false; + } + + // Run install script + { + info << "Running " << service_info.template_name << " install script on " << server << "..." << std::endl; + server_env.run_remote_template_command(service, "install", {}, false, {}); + } + + // print health tick + info << "Health: " << shared_commands::healthtick(server, service) << std::endl; + return true; } - // Run install script - { - info << "Running " << service_info.template_name << " install script on " << server << "..." << std::endl; - server_env.run_remote_template_command(service, "install", {}, silent, {}); - } - - // print health tick - info << "Health: " << shared_commands::healthtick(server, service) << std::endl; - return true; - } - + } // namespace shared_commands // ------------------------------------------------------------------------------------------------ // update_dropshell @@ -305,14 +308,15 @@ namespace dropshell info << "done." << std::endl; // add in bb64. We can't use execute_remote_command() here, as that relies on bb64 which we're installing! - info << "Installing bb64 on " << server << "..." << std::endl << std::flush; + info << "Installing bb64 on " << server << "..." << std::endl + << std::flush; std::string remote_cmd = "ssh -p " + server_env.get_SSH_INFO().port + " " + server_env.get_SSH_INFO().user + "@" + server_env.get_SSH_INFO().host + " 'mkdir -p " + quote(agent_path) + " && curl -fsSL \"https://gitea.jde.nz/public/bb64/releases/download/latest/install.sh\" | bash -s -- " + quote(agent_path) + " " + quote("$(id -u " + server_env.get_SSH_USER() + "):$(id -g " + server_env.get_SSH_USER() + ")") + "'"; - //std::cout << "Executing: " << remote_cmd << std::endl; + // std::cout << "Executing: " << remote_cmd << std::endl; if (!execute_local_command("", remote_cmd, {}, nullptr, cMode::Silent)) error << "Failed to download bb64 to " << agent_path << " on remote server." << std::endl; else @@ -327,7 +331,7 @@ namespace dropshell } info << "Installation on " << server << " complete." << std::endl; - return 0; + return 0; } // ------------------------------------------------------------------------------------------------ @@ -362,7 +366,7 @@ namespace dropshell std::vector services = get_server_services_info(server); for (const auto &service : services) { - if (!install_service(server, service.service_name, false)) + if (!shared_commands::install_service(server, service.service_name)) okay = false; } return okay ? 0 : 1; @@ -370,7 +374,7 @@ namespace dropshell else { // install the specific service. std::string service = safearg(ctx.args, 1); - return install_service(server, service, false) ? 0 : 1; + return shared_commands::install_service(server, service) ? 0 : 1; } } diff --git a/source/src/commands/nuke.cpp b/source/src/commands/nuke.cpp index 5d41d30..dc52de1 100644 --- a/source/src/commands/nuke.cpp +++ b/source/src/commands/nuke.cpp @@ -10,27 +10,29 @@ #include "utils/assert.hpp" -namespace dropshell { +namespace dropshell +{ -int nuke_handler(const CommandContext& ctx); -static std::vector nuke_name_list={"nuke"}; + int nuke_handler(const CommandContext &ctx); + static std::vector nuke_name_list = {"nuke"}; -// Static registration -struct NukeCommandRegister { - NukeCommandRegister() { - CommandRegistry::instance().register_command({ - nuke_name_list, - nuke_handler, - shared_commands::std_autocomplete, - false, // hidden - true, // requires_config - true, // requires_install - 2, // min_args (after command) - 2, // max_args (after command) - "nuke SERVER SERVICE|all", - "Nuke a service on a server. Destroys everything, both local and remote!", - // heredoc - R"( + // Static registration + struct NukeCommandRegister + { + NukeCommandRegister() + { + CommandRegistry::instance().register_command({nuke_name_list, + nuke_handler, + shared_commands::std_autocomplete, + false, // hidden + true, // requires_config + true, // requires_install + 2, // min_args (after command) + 2, // max_args (after command) + "nuke SERVER SERVICE|all", + "Nuke a service on a server. Destroys everything, both local and remote!", + // heredoc + R"( Nuke a service. Examples: @@ -41,109 +43,112 @@ struct NukeCommandRegister { both on the dropshell host and on the remote server. Use with caution! - )" - }); - } -} nuke_command_register; + )"}); + } + } nuke_command_register; -int nuke_one(std::string server, std::string service) -{ - server_env_manager server_env(server); - - // step 1 - nuke on remote server. - if (server_env.is_valid()) + namespace shared_commands { - LocalServiceInfo service_info; - service_info = get_service_info(server, service); - if (!SIvalid(service_info)) - error << "Warning: Invalid service: " << service << std::endl; - - if (server_env.check_remote_dir_exists(remotepath::service(server, service))) + bool nuke_service(const std::string &server, const std::string &service) { - // run the nuke script on the remote server if it exists. - // otherwise just uninstall. - if (gTemplateManager().template_command_exists(service_info.template_name, "nuke")) + server_env_manager server_env(server); + + // step 1 - nuke on remote server. + if (server_env.is_valid()) { - info << "Running nuke script for " << service << " on " << server << std::endl; - if (!server_env.run_remote_template_command(service, "nuke", {}, false, {})) - warning << "Failed to run nuke script: " << service << std::endl; + LocalServiceInfo service_info; + + service_info = get_service_info(server, service); + if (!SIvalid(service_info)) + error << "Warning: Invalid service: " << service << std::endl; + + if (server_env.check_remote_dir_exists(remotepath::service(server, service))) + { + // run the nuke script on the remote server if it exists. + // otherwise just uninstall. + if (gTemplateManager().template_command_exists(service_info.template_name, "nuke")) + { + info << "Running nuke script for " << service << " on " << server << std::endl; + if (!server_env.run_remote_template_command(service, "nuke", {}, false, {})) + warning << "Failed to run nuke script: " << service << std::endl; + } + else + { + info << "No nuke script found for " << service << " on " << server << std::endl; + info << "Running uninstall script instead and will clean directories." << std::endl; + if (!server_env.run_remote_template_command(service, "uninstall", {}, false, {})) + warning << "Failed to uninstall service: " << service << std::endl; + } + + // Remove the service directory from the server, running in a docker container as root. + if (server_env.remove_remote_dir(remotepath::service(server, service), true)) + { + ASSERT(!server_env.check_remote_dir_exists(remotepath::service(server, service)), "Service directory still found on server after uninstall"); + info << "Remote service directory removed: " << remotepath::service(server, service) << std::endl; + } + else + warning << "Failed to remove remote service directory" << std::endl; + } + else + warning << "Service not found on remote server: " << remotepath::service(server, service) << std::endl; + } + else + warning << "Can't nuke the remote service as the server is invalid: " << server << std::endl; + + // step 2 - nuke the local service directory. + std::string local_service_path = localpath::service(server, service); + if (local_service_path.empty() || !std::filesystem::exists(local_service_path)) + { + warning << "Local service directory not found: " << local_service_path << std::endl; } else { - info << "No nuke script found for " << service << " on " << server << std::endl; - info << "Running uninstall script instead and will clean directories." << std::endl; - if (!server_env.run_remote_template_command(service, "uninstall", {}, false, {})) - warning << "Failed to uninstall service: " << service << std::endl; + auto itemsdeleted = std::filesystem::remove_all(local_service_path); + if (itemsdeleted == 0) + error << "Failed to remove local service directory" << std::endl; } - // Remove the service directory from the server, running in a docker container as root. - if (server_env.remove_remote_dir(remotepath::service(server, service), true)) + info << "Nuked service " << service << " on server " << server << std::endl; + + return true; + } + } // namespace shared_commands + + int nuke_handler(const CommandContext &ctx) + { + ASSERT(ctx.args.size() == 2, "Usage: nuke SERVER SERVICE|all (requires 2 args - you supplied " + std::to_string(ctx.args.size()) + ")"); + ASSERT(gConfig().is_config_set(), "No configuration found. Please run 'dropshell config' to set up your configuration."); + + std::string server = safearg(ctx.args, 0); + std::string service = safearg(ctx.args, 1); + + if (service == "all") + { + int rval = 0; + + // iterate through all service folders in the server directory. + std::string server_path = localpath::server(server); + if (server_path.empty()) { - ASSERT(!server_env.check_remote_dir_exists(remotepath::service(server, service)), "Service directory still found on server after uninstall"); - info << "Remote service directory removed: " << remotepath::service(server, service) << std::endl; + error << "Server not found: " << server << std::endl; + return 1; } - else - warning << "Failed to remove remote service directory" << std::endl; + + for (const auto &entry : std::filesystem::directory_iterator(server_path)) + { + if (entry.is_directory() && entry.path().filename().string().find(".") != 0) + { + std::string service_name = entry.path().filename().string(); + rval |= (shared_commands::nuke_service(server, service_name) ? 0 : 1); + } + } + return rval; } else - warning << "Service not found on remote server: " << remotepath::service(server, service) << std::endl; - } - else - warning << "Can't nuke the remote service as the server is invalid: " << server << std::endl; - - // step 2 - nuke the local service directory. - std::string local_service_path = localpath::service(server, service); - if (local_service_path.empty() || !std::filesystem::exists(local_service_path)) - { - warning << "Local service directory not found: " << local_service_path << std::endl; - } - else - { - auto itemsdeleted = std::filesystem::remove_all(local_service_path); - if (itemsdeleted == 0) - error << "Failed to remove local service directory" << std::endl; - } - - info << "Nuked service " << service << " on server " << server << std::endl; - - return 0; -} - -int nuke_handler(const CommandContext &ctx) -{ - ASSERT(ctx.args.size() == 2, "Usage: nuke SERVER SERVICE|all (requires 2 args - you supplied " + std::to_string(ctx.args.size()) + ")"); - ASSERT(gConfig().is_config_set(), "No configuration found. Please run 'dropshell config' to set up your configuration."); - - std::string server = safearg(ctx.args, 0); - std::string service = safearg(ctx.args, 1); - - if (service == "all") - { - int rval = 0; - - // iterate through all service folders in the server directory. - std::string server_path = localpath::server(server); - if (server_path.empty()) { - error << "Server not found: " << server << std::endl; - return 1; + return (shared_commands::nuke_service(server, service) ? 0 : 1); } - - for (const auto& entry : std::filesystem::directory_iterator(server_path)) - { - if (entry.is_directory() && entry.path().filename().string().find(".") != 0) - { - std::string service_name = entry.path().filename().string(); - rval |= nuke_one(server, service_name); - } - } - return rval; } - else - { - return nuke_one(server, service); - } -} } // namespace dropshell \ No newline at end of file diff --git a/source/src/commands/restoredata.cpp b/source/src/commands/restoredata.cpp index fbbe939..096319b 100644 --- a/source/src/commands/restoredata.cpp +++ b/source/src/commands/restoredata.cpp @@ -16,122 +16,228 @@ #include "utils/directories.hpp" #include "shared_commands.hpp" +namespace dropshell +{ -namespace dropshell { + int restoredata_handler(const CommandContext &ctx); -int restoredata_handler(const CommandContext& ctx); + void restoredata_autocomplete(const CommandContext &ctx); -void restoredata_autocomplete(const CommandContext& ctx); + static std::vector restoredata_name_list = {"restoredata", "rd", "restore", "rest"}; - -static std::vector restoredata_name_list={"restoredata","rd","restore","rest"}; - -// Static registration -struct RestoreDataCommandRegister { - RestoreDataCommandRegister() { - CommandRegistry::instance().register_command({ - restoredata_name_list, - restoredata_handler, - restoredata_autocomplete, - false, // hidden - true, // requires_config - true, // requires_install - 3, // min_args (after command) - 3, // max_args (after command) - "restoredata SERVER SERVICE BACKUP_FILE", - "Restore data for a service on a server, overwriting the existing data.", - // heredoc - R"( + // Static registration + struct RestoreDataCommandRegister + { + RestoreDataCommandRegister() + { + CommandRegistry::instance().register_command({restoredata_name_list, + restoredata_handler, + restoredata_autocomplete, + false, // hidden + true, // requires_config + true, // requires_install + 3, // min_args (after command) + 3, // max_args (after command) + "restoredata SERVER SERVICE BACKUP_FILE|latest", + "Restore data for a service on a server, overwriting the existing data.", + // heredoc + R"( restoredata SERVER SERVICE BACKUP_FILE Restore data to a service on a server. Destructive. + restoredata SERVER SERVICE latest Restore the latest backup for the given service. Note: This command will not create any service configuration, you need to have a valid service installed first. The backup file must be in the local backups directory. WARNING: This will permanently overwrite the service's data on the remote server! - )" - }); - } -} restoredata_command_register; + )"}); + } + } restoredata_command_register; - -int restoredata_handler(const CommandContext& ctx) -{ - ASSERT(ctx.args.size() == 2, "Invalid number of arguments"); - std::string server = ctx.args[0]; - std::string service = ctx.args[1]; - std::string backup_file = ctx.args[2]; - - server_env_manager server_env(server); - if (!server_env.is_valid()) { - error << "Server " << server << " is not valid" << std::endl; - return 1; - } - - shared_commands::cBackupFileName backup_details(backup_file); - if (!backup_details.is_valid()) { - error << "Invalid backup file: " << backup_file << std::endl; - return 1; - } - - - - - return 0; -} - -void restoredata_autocomplete(const CommandContext& ctx) -{ - shared_commands::std_autocomplete(ctx); - if (ctx.args.size() == 2) // next arg is the backup file + std::vector get_backup_files(const std::string &server, const std::string &match_service = "", const std::string &match_template_name = "") { - std::string server = ctx.args[0]; - std::string service = ctx.args[1]; + std::string local_backups_dir = gConfig().get_local_backup_path(); + if (local_backups_dir.empty() || !std::filesystem::exists(local_backups_dir)) + { + error << "Error: Local backups directory not found: " << local_backups_dir << std::endl; + return {}; + } std::vector backups; - - LocalServiceInfo service_info = get_service_info(server, service); - if (!SIvalid(service_info)) { - error << "Service " << service << " is not valid" << std::endl; - return; - } - std::string template_name = service_info.template_name; - - std::string local_backups_dir = gConfig().get_local_backup_path(); - if (local_backups_dir.empty() || !std::filesystem::exists(local_backups_dir)) { - error << "Error: Local backups directory not found: " << local_backups_dir << std::endl; - return; - } - for (const auto& entry : std::filesystem::directory_iterator(local_backups_dir)) { - if (!entry.is_regular_file()) continue; + for (const auto &entry : std::filesystem::directory_iterator(local_backups_dir)) + { + if (!entry.is_regular_file()) + continue; std::string filename = entry.path().filename().string(); shared_commands::cBackupFileName backup_details(filename); - if (backup_details.is_valid()) { - if (backup_details.get_template_name() == template_name) { - backups.push_back(backup_details); - } - } + if (backup_details.is_valid()) + if (match_service.empty() || backup_details.get_service() == match_service) + if (match_template_name.empty() || backup_details.get_template_name() == match_template_name) + backups.push_back(backup_details); } // sort backups by datetime - std::sort(backups.begin(), backups.end(), [](const shared_commands::cBackupFileName& a, const shared_commands::cBackupFileName& b) { - return a.get_datetime() > b.get_datetime(); - }); + std::sort(backups.begin(), backups.end(), [](const shared_commands::cBackupFileName &a, const shared_commands::cBackupFileName &b) + { return a.get_datetime() > b.get_datetime(); }); - // print most recent backup for each {host,service} pair - std::map unique_backups; - for (const auto& backup : backups) { - std::string key = backup.get_server() + "-" + backup.get_service(); - if (unique_backups.find(key) == unique_backups.end()) { - unique_backups[key] = backup.get_filename(); - } + return backups; + } + + + int restoredata_handler(const CommandContext &ctx) + { + ASSERT(ctx.args.size() == 3, "Invalid number of arguments"); + std::string server = ctx.args[0]; + std::string service = ctx.args[1]; + std::string backup_file = ctx.args[2]; + + server_env_manager server_env(server); + if (!server_env.is_valid()) + { + error << "Server " << server << " is not valid" << std::endl; + return 1; } - for (const auto& [key, value] : unique_backups) { - rawout << value << std::endl; + LocalServiceInfo service_info = get_service_info(server, service); + if (!SIvalid(service_info)) + { + error << "Service " << service << " is not valid" << std::endl; + return 1; + } + + std::optional backup_details; + if (backup_file == "latest") + { // special case. + std::vector backups = get_backup_files(server, service, service_info.template_name); // this service only (and also match template in case something changed there!). + if (backups.empty()) + { + error << "No backups found for " << server << "/" << service << std::endl; + debug << "Template also has to match with the service template: " << service_info.template_name << std::endl; + return 1; + } + backup_details = backups[0]; + } else { + backup_details = shared_commands::cBackupFileName(backup_file); + } + + bool backup_details_okay = backup_details.has_value() && backup_details->is_valid(); + if (!backup_details_okay) + { + error << "Invalid backup file: " << backup_file << std::endl; + return 1; + } + + std::string local_backups_dir = gConfig().get_local_backup_path(); + if (local_backups_dir.empty() || !std::filesystem::exists(local_backups_dir)) + { + error << "Error: Local backups directory not found: " << local_backups_dir << std::endl; + return 1; + } + std::string local_backup_file_path = (std::filesystem::path(local_backups_dir) / backup_file).string(); + if (!std::filesystem::exists(local_backup_file_path)) + { + error << "Error: Backup file not found at " << local_backup_file_path << std::endl; + return 1; + } + + if (backup_details->get_template_name() != service_info.template_name) + { + error << "Error: Backup template does not match service template. Can't restore." << std::endl; + debug << "Backup template: " << backup_details->get_template_name() << std::endl; + debug << "Service template: " << service_info.template_name << std::endl; + debug << "Backup file: " << local_backup_file_path << std::endl; + return 1; + } + + info << "Restoring " << backup_details->get_datetime() << " backup of " << backup_details->get_service() << " taken from " << backup_details->get_server() << ", onto " << server << "/" << service << std::endl; + info << std::endl; + warning << "*** ALL DATA FOR " << server << "/" << service << " WILL BE OVERWRITTEN! ***" << std::endl; + + // run the restore script + info << "OK, here goes..." << std::endl; + + { // backup existing service + info << "1) Backing up old service... " << std::endl; + if (!shared_commands::backupdata_service(server, service)) + { + error << "Error: Backup failed, restore aborted." << std::endl; + error << "You can try using dropshell install " << server << " " << service << " to install the service afresh." << std::endl; + error << "Otherwise, stop the service, create and initialise a new one, then restore to that." << std::endl; + return 1; + } + info << "Backup complete." << std::endl; + } + + { // nuke the old service + info << "2) Nuking old service..." << std::endl; + if (!shared_commands::nuke_service(server, service)) + return 1; + } + + { // restore service from backup + info << "3) Restoring service data from backup..." << std::endl; + std::string remote_backups_dir = remotepath::backups(server); + std::string remote_backup_file_path = remote_backups_dir + "/" + backup_file; + + // Copy backup file from local to server + if (!shared_commands::scp_file_to_remote(server_env, local_backup_file_path, remote_backup_file_path, false)) + { + error << "Failed to copy backup file from local to server" << std::endl; + return 1; + } + + shared_commands::cRemoteTempFolder remote_temp_folder(server_env); + server_env.run_remote_template_command(service, "restore", {}, false, {{"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 + info << "4) Non-destructive install of fresh service..." << std::endl; + if (!shared_commands::install_service(server, service)) + return 1; + } + + { // healthcheck the service + info << "5) Healthchecking service..." << std::endl; + std::string green_tick = "\033[32m✓\033[0m"; + std::string red_cross = "\033[31m✗\033[0m"; + bool healthy = (server_env.run_remote_template_command(service, "status", {}, false, {})); + info << (healthy ? green_tick : red_cross) << " Service is " << (healthy ? "healthy" : "NOT healthy") << std::endl; + } + + return 0; + } + + + + void restoredata_autocomplete(const CommandContext &ctx) + { + shared_commands::std_autocomplete(ctx); + if (ctx.args.size() == 2) // next arg is the backup file + { + std::string server = ctx.args[0]; + std::string service = ctx.args[1]; + + LocalServiceInfo service_info = get_service_info(server, service); + if (!SIvalid(service_info)) + { + error << "Service " << service << " is not valid" << std::endl; + return; + } + std::string template_name = service_info.template_name; + + std::vector backups = get_backup_files(server, "", template_name); // any service, but must match template. + + // print most recent backup for each {host,service} pair + std::map unique_backups; + for (const auto &backup : backups) + { + std::string key = backup.get_server() + "-" + backup.get_service(); + if (unique_backups.find(key) == unique_backups.end()) + unique_backups[key] = backup.get_filename(); + } + for (const auto &[key, value] : unique_backups) + rawout << value << std::endl; } } -} - } // namespace dropshell \ No newline at end of file diff --git a/source/src/commands/shared_commands.hpp b/source/src/commands/shared_commands.hpp index ce21123..c11bf6d 100644 --- a/source/src/commands/shared_commands.hpp +++ b/source/src/commands/shared_commands.hpp @@ -81,6 +81,18 @@ namespace dropshell bool scp_file_to_remote(const server_env_manager &server_env, const std::string &local_path, const std::string &remote_path, bool silent); bool scp_file_from_remote(const server_env_manager &server_env, const std::string &remote_path, const std::string &local_path, bool silent); + // defined in backupdata.cpp, used by restoredata.cpp. + bool backupdata_service(const std::string& server, const std::string& service); + + // defined in uninstall.cpp + bool uninstall_service(const std::string &server, const std::string &service); + + // defined in nuke.cpp + bool nuke_service(const std::string &server, const std::string &service); + + // defined in install.cpp + bool install_service(const std::string &server, const std::string &service); + } // namespace shared_commands } // namespace dropshell #endif diff --git a/source/src/commands/uninstall.cpp b/source/src/commands/uninstall.cpp index e899bf1..e09ad99 100644 --- a/source/src/commands/uninstall.cpp +++ b/source/src/commands/uninstall.cpp @@ -41,45 +41,43 @@ namespace dropshell } } uninstall_command_register; - bool uninstall_service(const std::string &server, const std::string &service, bool silent = false) - { - if (!silent) + namespace shared_commands { + bool uninstall_service(const std::string &server, const std::string &service) + { maketitle("Uninstalling " + service + " on " + server); - server_env_manager server_env(server); - if (!server_env.is_valid()) - { - error << "Invalid server: " << server << std::endl; - return false; // should never hit this. - } + server_env_manager server_env(server); + if (!server_env.is_valid()) + { + error << "Invalid server: " << server << std::endl; + return false; // should never hit this. + } - // 2. Check if service directory exists on server - if (!server_env.check_remote_dir_exists(remotepath::service(server, service))) - { - error << "Service is not installed: " << service << std::endl; - return true; // Nothing to uninstall - } + // 2. Check if service directory exists on server + if (!server_env.check_remote_dir_exists(remotepath::service(server, service))) + { + error << "Service is not installed: " << service << std::endl; + return true; // Nothing to uninstall + } - // 3. Run uninstall script if it exists - std::string uninstall_script = remotepath::service_template(server, service) + "/uninstall.sh"; - if (!server_env.run_remote_template_command(service, "uninstall", {}, silent, {})) - if (!silent) + // 3. Run uninstall script if it exists + std::string uninstall_script = remotepath::service_template(server, service) + "/uninstall.sh"; + if (!server_env.run_remote_template_command(service, "uninstall", {}, false, {})) warning << "Uninstall script failed, but continuing with directory removal" << std::endl; - // 4. Remove the service directory from the server, running in a docker container as root. - if (server_env.remove_remote_dir(remotepath::service(server, service), silent)) - { - ASSERT(!server_env.check_remote_dir_exists(remotepath::service(server, service)), "Service directory still found on server after uninstall"); - if (!silent) + // 4. Remove the service directory from the server, running in a docker container as root. + if (server_env.remove_remote_dir(remotepath::service(server, service), false)) + { + ASSERT(!server_env.check_remote_dir_exists(remotepath::service(server, service)), "Service directory still found on server after uninstall"); info << "Removed remote service directory " << remotepath::service(server, service) << std::endl; - } - else if (!silent) - warning << "Failed to remove remote service directory" << std::endl; + } + else + warning << "Failed to remove remote service directory" << std::endl; - if (!silent) info << "Completed service " << service << " uninstall on " << server << std::endl; - return true; - } + return true; + } + } // namespace shared_commands int uninstall_handler(const CommandContext &ctx) { @@ -98,14 +96,14 @@ namespace dropshell std::vector services = get_server_services_info(server); for (const auto &service : services) { - if (!uninstall_service(server, service.service_name)) + if (!shared_commands::uninstall_service(server, service.service_name)) okay = false; } return okay ? 0 : 1; } std::string service = safearg(ctx.args, 1); - return uninstall_service(server, service) ? 0 : 1; + return shared_commands::uninstall_service(server, service) ? 0 : 1; } } // namespace dropshell \ No newline at end of file