#include "command_registry.hpp" #include "config.hpp" #include "utils/utils.hpp" #include "utils/directories.hpp" #include "templates.hpp" #include "shared_commands.hpp" #include "utils/hash.hpp" #include "utils/createagent.hpp" #include <unistd.h> #include <cstring> #include <iostream> #include <sstream> #include <filesystem> #include "utils/assert.hpp" namespace dropshell { int install_handler(const CommandContext &ctx); static std::vector<std::string> install_name_list = {"install", "reinstall", "update"}; // Static registration struct InstallCommandRegister { InstallCommandRegister() { CommandRegistry::instance().register_command({install_name_list, install_handler, std_autocomplete_allowallservices, false, // hidden false, // requires_config false, // requires_install 0, // min_args (after command) 2, // max_args (after command) "install SERVER [SERVICE]", "Install/reinstall service(s). Safe/non-destructive way to update.", // heredoc R"( Install components on a server. This is safe to re-run (non-destructive) and used to update servers and their services. install (re)install dropshell components on this computer. install SERVER (re)install dropshell agent on the given server. install SERVER [SERVICE|*] (re)install the given service (or all services) on the given server. Note you need to create the service first with: dropshell create-service <server> <template> <service> )"}); } } install_command_register; // ------------------------------------------------------------------------------------------------ // rsync_tree_to_remote : SHARED COMMAND // ------------------------------------------------------------------------------------------------ bool rsync_tree_to_remote( const std::string &local_path, const std::string &remote_path, server_env_manager &server_env, bool silent) { ASSERT(!local_path.empty() && !remote_path.empty(), "Local or remote path not specified. Can't rsync."); std::string rsync_cmd = "rsync --delete --mkpath -zrpc -e 'ssh -p " + server_env.get_SSH_PORT() + "' " + quote(local_path + "/") + " " + quote(server_env.get_SSH_USER() + "@" + server_env.get_SSH_HOST() + ":" + remote_path + "/"); return execute_local_command(rsync_cmd, nullptr, (silent ? cMode::Silent : cMode::Defaults)); } // ------------------------------------------------------------------------------------------------ // install service over ssh : SHARED COMMAND // ------------------------------------------------------------------------------------------------ bool install_service(const std::string &server, const std::string &service, bool silent) { 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; // 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 std::cout << "Copying: [LOCAL] " << tinfo.local_template_path() << std::endl << std::string(8, ' ') << "[REMOTE] " << remotepath::service_template(server, service) << "/" << std::endl; if (!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; } // Copy service files std::cout << "Copying: [LOCAL] " << localpath::service(server, service) << std::endl << std::string(8, ' ') << "[REMOTE] " << remotepath::service_config(server, service) << std::endl; if (!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; } // Run install script { server_env.run_remote_template_command(service, "install", {}, silent, {}); } // print health tick std::cout << "Health: " << healthtick(server, service) << std::endl; return true; } // ------------------------------------------------------------------------------------------------ // get_arch : SHARED COMMAND // ------------------------------------------------------------------------------------------------ std::string get_arch() { // determine the architecture of the system std::string arch; #ifdef __aarch64__ arch = "arm64"; #elif __x86_64__ arch = "amd64"; #endif return arch; } // ------------------------------------------------------------------------------------------------ // update_dropshell // ------------------------------------------------------------------------------------------------ std::string _exec(const char *cmd) { char buffer[128]; std::string result = ""; FILE *pipe = popen(cmd, "r"); if (!pipe) { throw std::runtime_error("popen() failed!"); } while (!feof(pipe)) { if (fgets(buffer, 128, pipe) != nullptr) result += buffer; } pclose(pipe); return trim(result); } int update_dropshell() { // determine path to this executable std::filesystem::path dropshell_path = std::filesystem::canonical("/proc/self/exe"); std::filesystem::path parent_path = dropshell_path.parent_path(); // determine the architecture of the system std::string arch = get_arch(); std::string url = "https://gitea.jde.nz/public/dropshell/releases/download/latest/dropshell." + arch; // download new version, preserve permissions and ownership std::string bash_script; bash_script += "docker run --rm -v " + parent_path.string() + ":/target"; bash_script += " gitea.jde.nz/public/debian-curl:latest"; bash_script += " sh -c \""; bash_script += " curl -fsSL " + url + " -o /target/dropshell_temp &&"; bash_script += " chmod --reference=/target/dropshell /target/dropshell_temp &&"; bash_script += " chown --reference=/target/dropshell /target/dropshell_temp"; bash_script += "\""; std::string cmd = "bash -c '" + bash_script + "'"; int rval = system(cmd.c_str()); if (rval != 0) { std::cerr << "Failed to download new version of dropshell." << std::endl; return -1; } // check if the new version is the same as the old version uint64_t new_hash = hash_file(parent_path / "dropshell_temp"); uint64_t old_hash = hash_file(parent_path / "dropshell"); if (new_hash == old_hash) { std::cout << "Confirmed dropshell is the latest version." << std::endl; return 0; } std::string runvercmd = (parent_path / "dropshell").string() + " version"; std::string currentver = _exec(runvercmd.c_str()); runvercmd = (parent_path / "dropshell_temp").string() + " version"; std::string newver = _exec(runvercmd.c_str()); if (currentver >= newver) { std::cout << "Current dropshell version: " << currentver << ", published version: " << newver << std::endl; std::cout << "No update needed." << std::endl; return 0; } return 0; std::string bash_script_2 = "docker run --rm -v " + parent_path.string() + ":/target gitea.jde.nz/public/debian-curl:latest " + "sh -c \"mv /target/dropshell_temp /target/dropshell\""; rval = system(bash_script_2.c_str()); if (rval != 0) { std::cerr << "Failed to install new version of dropshell." << std::endl; return -1; } std::cout << "Successfully updated " << dropshell_path << " to the latest " << arch << " version." << std::endl; // execute the new version execlp("bash", "bash", "-c", (parent_path / "dropshell").c_str(), "install", (char *)nullptr); std::cerr << "Failed to execute new version of dropshell." << std::endl; return -1; } int install_local_agent() { std::vector<std::filesystem::path> paths = { gConfig().get_local_template_cache_path(), gConfig().get_local_backup_path(), gConfig().get_local_tempfiles_path(), localpath::agent()}; for (auto &p : gConfig().get_local_server_definition_paths()) paths.push_back(p); for (auto &p : paths) if (!std::filesystem::exists(p)) { std::cout << "Creating directory: " << p << std::endl; std::filesystem::create_directories(p); } // download bb64. if (!std::filesystem::exists(localpath::agent() + "bb64")) { std::string cmd = "cd " + localpath::agent() + " && curl -fsSL -o bb64 https://gitea.jde.nz/public/bb64/releases/download/latest/bb64.amd64 && chmod a+x bb64"; int ret = system(cmd.c_str()); if (EXITSTATUSCHECK(ret)) std::cout << "Downloaded bb64 to " << localpath::agent() << std::endl; else std::cerr << "Failed to download bb64 to " << localpath::agent() << std::endl; } else { std::cout << "Updating bb64..." << std::endl; system((localpath::agent() + "bb64 -u").c_str()); // update. } return 0; } int install_host() { // update dropshell. // install the local dropshell agent. int rval = update_dropshell(); if (rval != 0) return rval; rval = install_local_agent(); if (rval != 0) return rval; std::cout << "Installation complete." << std::endl; return 0; } int install_server(const std::string &server) { // install the dropshell agent on the given server. std::cout << "Installing dropshell agent on " << server << std::endl; std::string agent_path = remotepath::agent(server); if (agent_path.empty()) { std::cerr << "Failed to get agent path for " << server << std::endl; return 1; } server_env_manager server_env(server); if (!server_env.is_valid()) { std::cerr << "Invalid server environment for " << server << std::endl; return 1; } // first install bb64. std::cout << "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; if (!execute_local_command(remote_cmd, nullptr, cMode::Silent)) std::cerr << "Failed to download bb64 to " << agent_path << " on remote server." << std::endl; else std::cout << "Downloaded bb64 to " << agent_path << " on remote server." << std::endl; // now create the agent. create_agent(server); return 0; // NOTIMPL } // ------------------------------------------------------------------------------------------------ // install command implementation // ------------------------------------------------------------------------------------------------ int install_handler(const CommandContext &ctx) { if (ctx.args.size() < 1) { // install host return install_host(); } std::string server = safearg(ctx.args, 0); if (ctx.args.size() == 1) { // install server return install_server(server); } // install service(s) if (safearg(ctx.args, 1) == "*") { // install all services on the server bool okay = true; std::vector<LocalServiceInfo> services = get_server_services_info(server); for (const auto &service : services) { if (!install_service(server, service.service_name, false)) okay = false; } return okay ? 0 : 1; } else { // install the specific service. std::string service = safearg(ctx.args, 1); return install_service(server, service, false) ? 0 : 1; } } } // namespace dropshell