dropshell/src/main_commands.cpp
2025-04-27 19:56:44 +12:00

275 lines
11 KiB
C++

#include "main_commands.hpp"
#include "utils/directories.hpp"
#include "utils/utils.hpp"
#include "utils/readmes.hpp"
#include "service_runner.hpp"
#include "config.hpp"
#include "templates.hpp"
#include "server_env.hpp"
#include <iostream>
#include <filesystem>
#include <algorithm>
namespace dropshell {
namespace main_commands {
static const std::string magic_string = "-_-";
int init(const std::vector<std::string> &args)
{
dropshell::config *cfg = dropshell::get_global_config();
std::string lcd;
if (args.size() < 3) {
std::cerr << "Error: init command requires a directory argument" << std::endl;
return 1;
}
try {
if (!cfg->add_local_config_directory(args[2]))
return 1; // error already reported
cfg->save_config();
std::cout << "Config directory added: " << cfg->get_local_config_directories().back() << std::endl;
dropshell::create_readme_local_config_dir(cfg->get_local_config_directories().back());
if (cfg->get_local_config_directories().size() ==1)
std::cout << "DropShell is now initialised and you can add a server with 'dropshell create-server <server-name>'" << std::endl;
else
{
std::cout << "DropShell will now use all of the following directories for configuration:" << std::endl;
for (const auto& dir : cfg->get_local_config_directories()) {
std::cout << " " << dir << std::endl;
}
std::cout << "You can edit the config file manually with: dropshell edit" << std::endl;
}
return 0;
} catch (const std::exception& e) {
std::cerr << "Error in init: " << e.what() << std::endl;
return 1;
}
}
int restore(const std::vector<std::string> &args)
{
if (args.size() < 4) {
std::cerr << "Error: not enough arguments. dropshell restore <server> <service> <backup-file>" << std::endl;
return 1;
}
std::string server_name = args[2];
std::string service_name = args[3];
std::string backup_file = args[4];
ServiceInfo 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 env(server_name);
if (!env.is_valid()) {
std::cerr << "Error: Invalid server environment" << std::endl;
return 1;
}
std::string local_backups_dir = get_local_backup_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<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 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 "<<backup_server_name<<", onto "<<server_name<<std::endl;
std::cout << std::endl;
std::cout << "*** ALL DATA FOR "<<server_name<<"/"<<service_name<<" WILL BE OVERWRITTEN! ***"<<std::endl;
std::cout << std::endl;
std::cout << "Are you sure you want to continue? (y/n)" << std::endl;
char confirm;
std::cin >> confirm;
if (confirm != 'y') {
std::cout << "Restore cancelled." << std::endl;
return 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<std::string> 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 "<<server_name<<" "<<service_name<<" 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 1;
}
std::cout << "Backup complete." << std::endl;
}
{ // silently uninstalling the current service
std::cout << "2) Uninstalling current service..." << std::endl;
env.run_remote_template_command(service_name, "uninstall", {}, true);
}
{ // restore service from backup
std::cout << "3) Restoring service from backup..." << std::endl;
env.run_remote_template_command(service_name, "restore", {local_backup_file_path}, true);
}
// healthcheck the service
std::cout << "4) Healthchecking service..." << std::endl;
std::string green_tick = "\033[32m✓\033[0m";
std::string red_cross = "\033[31m✗\033[0m";
if (!env.run_remote_template_command(service_name, "status", {}, true))
std::cout << green_tick << " Service is healthy." << std::endl;
else
std::cout << red_cross << " Service is 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<std::string> & 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];
ServiceInfo 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 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 = get_remote_service_template_path(server_name, service_name);
std::string remote_command_script_file = remote_service_template_path + "/" + command + ".sh";
std::string remote_service_config_path = get_remote_service_config_path(server_name, service_name);
if (!env.check_remote_items_exist({
get_remote_service_path(server_name, service_name),
remote_command_script_file,
get_remote_service_env_file(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 = get_remote_backups_path(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 = get_local_backup_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: " << local_backup_file_path << std::endl;
return true;
}
} // namespace main_commands
} // namespace dropshell