243 lines
11 KiB
C++
243 lines
11 KiB
C++
#include <unistd.h>
|
|
#include <cstring>
|
|
#include <iostream>
|
|
#include <sstream>
|
|
#include <filesystem>
|
|
|
|
#include "utils/output.hpp"
|
|
#include "utils/assert.hpp"
|
|
#include "utils/utils.hpp"
|
|
#include "command_registry.hpp"
|
|
#include "config.hpp"
|
|
#include "services.hpp"
|
|
#include "servers.hpp"
|
|
#include "server_env_manager.hpp"
|
|
#include "templates.hpp"
|
|
#include "utils/directories.hpp"
|
|
#include "shared_commands.hpp"
|
|
|
|
namespace dropshell
|
|
{
|
|
|
|
int restoredata_handler(const CommandContext &ctx);
|
|
|
|
void restoredata_autocomplete(const CommandContext &ctx);
|
|
|
|
static std::vector<std::string> 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|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;
|
|
|
|
|
|
std::vector<shared_commands::cBackupFileName> get_backup_files(const std::string &server, const std::string &match_service = "", const std::string &match_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 {};
|
|
}
|
|
|
|
std::vector<shared_commands::cBackupFileName> backups;
|
|
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 (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(); });
|
|
|
|
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;
|
|
}
|
|
LocalServiceInfo service_info = get_service_info(server, service);
|
|
if (!SIvalid(service_info))
|
|
{
|
|
error << "Service " << service << " is not valid" << std::endl;
|
|
return 1;
|
|
}
|
|
|
|
std::optional<shared_commands::cBackupFileName> backup_details;
|
|
if (backup_file == "latest")
|
|
{ // special case.
|
|
std::vector<shared_commands::cBackupFileName> 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<shared_commands::cBackupFileName> backups = get_backup_files(server, "", template_name); // any service, but must match template.
|
|
|
|
// print most recent backup for each {host,service} pair
|
|
std::map<std::string, std::string> 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
|