518 lines
20 KiB
C++
518 lines
20 KiB
C++
#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 "autogen/_agent-local.hpp"
|
|
#include "autogen/_agent-remote.hpp"
|
|
#include "services.hpp"
|
|
#include "utils/output.hpp"
|
|
|
|
#include <fstream>
|
|
#include <unistd.h>
|
|
#include <cstring>
|
|
#include <iostream>
|
|
#include <sstream>
|
|
#include <filesystem>
|
|
#include <libassert/assert.hpp>
|
|
#include "servers.hpp"
|
|
#include <sys/stat.h>
|
|
|
|
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,
|
|
shared_commands::std_autocomplete_allowall,
|
|
false, // hidden
|
|
false, // requires_config
|
|
false, // requires_install
|
|
0, // min_args (after command)
|
|
2, // max_args (after command)
|
|
"install [SERVER] [SERVICE|all]",
|
|
"Install/reinstall host and remote servers, or service(s). Safe 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, and on all servers.
|
|
install SERVER (re)install dropshell agent on the particular given server.
|
|
install SERVER [SERVICE|all] (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;
|
|
|
|
|
|
namespace shared_commands
|
|
{
|
|
|
|
// ------------------------------------------------------------------------------------------------
|
|
// install service over ssh : SHARED COMMAND
|
|
// ------------------------------------------------------------------------------------------------
|
|
bool install_service(const ServerConfig &server_env, const std::string &service)
|
|
{
|
|
std::string server = server_env.get_server_name();
|
|
LocalServiceInfo service_info = get_service_info(server_env.get_server_name(), service);
|
|
|
|
if (!SIvalid(service_info))
|
|
{
|
|
error << "Failed to install - service information not valid." << std::endl;
|
|
return false;
|
|
}
|
|
|
|
if (!server_env.is_valid())
|
|
return false; // should never hit this.
|
|
|
|
|
|
std::string user = service_info.user;
|
|
std::string remote_service_path = remotepath(server,user).service(service);
|
|
|
|
ASSERT(!remote_service_path.empty(), "Install_Service: Remote service path is empty for " + service + " on " + server);
|
|
ASSERT(!user.empty(), "Install_Service: User is empty for " + service + " on " + server);
|
|
|
|
if (server_env.check_remote_dir_exists(remote_service_path, user))
|
|
{ // uninstall the old service before we update the config or template!
|
|
info << "Service " << service << " is already installed on " << server << std::endl;
|
|
shared_commands::uninstall_service(server_env, service);
|
|
}
|
|
|
|
if (!service_info.service_template_hash_match)
|
|
{
|
|
warning << "Service " << service << " is using an old template. Updating. " << std::endl;
|
|
if (!merge_updated_service_template(server_env.get_server_name(), service))
|
|
{
|
|
error << "Failed to merge updated service template. " << std::endl;
|
|
return false;
|
|
}
|
|
|
|
service_info = get_service_info(server_env.get_server_name(), service);
|
|
if (!SIvalid(service_info) || !service_info.service_template_hash_match)
|
|
{
|
|
error << "Merged updated service template, but it is still not valid. " << std::endl;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
maketitle("Installing " + service + " (" + service_info.template_name + ") on " + server);
|
|
|
|
// 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 mkdir_cmd = "mkdir -p " + quote(remote_service_path);
|
|
if (!execute_ssh_command(server_env.get_SSH_INFO(user), sCommand("", mkdir_cmd, {}), cMode::Silent))
|
|
{
|
|
std::cerr << "Failed to create service directory " << remote_service_path << std::endl;
|
|
return false;
|
|
}
|
|
|
|
// Copy template files
|
|
debug << "Copying: [LOCAL] " << tinfo.local_template_path() << std::endl
|
|
<< std::string(8, ' ') << "[REMOTE] " << remotepath(server,user).service_template(service) << "/" << std::endl;
|
|
if (!shared_commands::rsync_tree_to_remote(tinfo.local_template_path().string(), remotepath(server,user).service_template(service),
|
|
server_env, false, service_info.user))
|
|
{
|
|
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
|
|
<< std::string(8, ' ') << "[REMOTE] " << remotepath(server,user).service_config(service) << std::endl;
|
|
if (!shared_commands::rsync_tree_to_remote(localpath::service(server, service), remotepath(server,user).service_config(service),
|
|
server_env, false, service_info.user))
|
|
{
|
|
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;
|
|
|
|
shared_commands::cRemoteTempFolder remote_temp_folder(server_env, user);
|
|
if (!server_env.run_remote_template_command(service, "install", {}, false, {{"TEMP_DIR", remote_temp_folder.path()}}))
|
|
{
|
|
error << "Failed to run install script on " << server << std::endl;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// print health tick
|
|
info << "Health: " << shared_commands::healthtick(server, service) << std::endl;
|
|
|
|
|
|
return true;
|
|
}
|
|
|
|
} // namespace shared_commands
|
|
|
|
// ------------------------------------------------------------------------------------------------
|
|
// 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 configure_autocomplete()
|
|
{
|
|
debug << "Ensuring dropshell autocomplete is registered in ~/.bashrc..." << std::endl;
|
|
|
|
std::filesystem::path bashrc = localpath::current_user_home() +"/.bashrc";
|
|
|
|
std::string autocomplete_script = R"(
|
|
#---DROPSHELL AUTOCOMPLETE START---
|
|
_dropshell_completions() {
|
|
local cur
|
|
COMPREPLY=()
|
|
cur="${COMP_WORDS[COMP_CWORD]}"
|
|
|
|
# call dropshell to get the list of possiblities for the current argument. Supply all previous arguments.
|
|
local completions=($(dropshell autocomplete "${COMP_WORDS[@]:1:${COMP_CWORD}-1}"))
|
|
COMPREPLY=( $(compgen -W "${completions[*]}" -- ${cur}) )
|
|
return 0
|
|
}
|
|
|
|
# Register the completion function
|
|
complete -F _dropshell_completions dropshell
|
|
complete -F _dropshell_completions ds
|
|
#---DROPSHELL AUTOCOMPLETE END---
|
|
)";
|
|
|
|
file_replace_or_add_segment(bashrc.string(), autocomplete_script);
|
|
return 0;
|
|
}
|
|
|
|
int configure_localbin()
|
|
{
|
|
debug << "Ensuring ~/.local/bin is in the ~/.bashrc path..." << std::endl;
|
|
|
|
std::filesystem::path bashrc = localpath::current_user_home() +"/.bashrc";
|
|
std::filesystem::path localbin = localpath::current_user_home() + "/.local/bin";
|
|
std::filesystem::create_directories(localbin);
|
|
// check if already in path
|
|
const char* env_p = std::getenv("PATH");
|
|
if (env_p) {
|
|
std::string path_str = env_p;
|
|
if (path_str.find(localbin.string()) == std::string::npos) {
|
|
std::string pathstr="#---DROPSHELL PATH START---\nexport PATH=\""+localbin.string()+":$PATH\"\n#---DROPSHELL PATH END---\n";
|
|
file_replace_or_add_segment(bashrc.string(), pathstr);
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
int update_dropshell()
|
|
{
|
|
maketitle("Updating dropshell on this computer...");
|
|
|
|
configure_localbin();
|
|
configure_autocomplete();
|
|
|
|
// determine path to this executable
|
|
std::filesystem::path exe_path = std::filesystem::canonical("/proc/self/exe");
|
|
std::filesystem::path parent_path = exe_path.parent_path();
|
|
|
|
// determine the architecture of the system
|
|
std::string arch = shared_commands::get_arch();
|
|
|
|
// check that the user that owns the exe is the current user this process is running as.
|
|
struct stat st;
|
|
if (stat(exe_path.c_str(), &st) != 0) {
|
|
error << "Failed to stat dropshell executable: " << strerror(errno) << std::endl;
|
|
return -1;
|
|
}
|
|
|
|
uid_t current_uid = getuid();
|
|
if (st.st_uid != current_uid) {
|
|
warning << "Current user does not own the dropshell executable. Please run as the owner to update." << std::endl;
|
|
return -1;
|
|
}
|
|
|
|
// Get current version (format: YYYY.MMDD.HHMM)
|
|
std::string runvercmd = exe_path.string() + " version";
|
|
std::string currentver = _exec(runvercmd.c_str());
|
|
currentver = trim(currentver);
|
|
|
|
info << "Current version: " << currentver << std::endl;
|
|
info << "Checking for updates..." << std::endl;
|
|
|
|
shared_commands::cLocalTempFolder local_temp_folder;
|
|
std::filesystem::path sos_path = local_temp_folder.path() / "sos";
|
|
|
|
// Download sos to check metadata
|
|
bool should_download = true;
|
|
if (!download_file("https://getbin.xyz/sos", sos_path))
|
|
{
|
|
warning << "Failed to download sos utility, proceeding with direct download." << std::endl;
|
|
}
|
|
else
|
|
{
|
|
chmod(sos_path.c_str(), 0755);
|
|
|
|
// Use sos to get metadata about the remote dropshell binary
|
|
std::string info_cmd = sos_path.string() + " info getbin.xyz dropshell:latest-" + arch;
|
|
std::string info_output = _exec(info_cmd.c_str());
|
|
|
|
// Parse the version from the info output
|
|
// The metadata should contain version in format: "version: YYYY.MMDD.HHMM"
|
|
std::size_t version_pos = info_output.find("version: ");
|
|
if (version_pos != std::string::npos)
|
|
{
|
|
std::string remote_version = info_output.substr(version_pos + 9);
|
|
remote_version = remote_version.substr(0, remote_version.find('\n'));
|
|
remote_version = trim(remote_version);
|
|
|
|
info << "Latest available version: " << remote_version << std::endl;
|
|
|
|
// Compare versions (both in YYYY.MMDD.HHMM format)
|
|
if (!remote_version.empty() && currentver >= remote_version)
|
|
{
|
|
info << "Dropshell is already up to date." << std::endl;
|
|
return 0;
|
|
}
|
|
|
|
if (!remote_version.empty() && currentver < remote_version)
|
|
{
|
|
info << "New version available, downloading..." << std::endl;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
debug << "Version information not found in metadata, proceeding with download." << std::endl;
|
|
}
|
|
}
|
|
|
|
if (!should_download)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
// Download new version from getbin.xyz
|
|
std::string url = "https://getbin.xyz/dropshell:latest-" + arch;
|
|
std::filesystem::path temp_file = local_temp_folder.path() / "dropshell";
|
|
|
|
bool download_okay = download_file(url, temp_file);
|
|
if (!download_okay)
|
|
{
|
|
error << "Failed to download new version of dropshell from " << url << std::endl;
|
|
return -1;
|
|
}
|
|
|
|
// make executable
|
|
chmod(temp_file.c_str(), 0755);
|
|
|
|
// Get version of downloaded binary to double-check
|
|
runvercmd = temp_file.string() + " version";
|
|
std::string newver = _exec(runvercmd.c_str());
|
|
newver = trim(newver);
|
|
|
|
// Final version check
|
|
if (currentver >= newver)
|
|
{
|
|
info << "Downloaded version (" << newver << ") is not newer than current (" << currentver << ")" << std::endl;
|
|
info << "No update needed." << std::endl;
|
|
return 0;
|
|
}
|
|
|
|
info << "Updating from version " << currentver << " to " << newver << std::endl;
|
|
|
|
// Backup old version and replace with new
|
|
std::filesystem::path backup_path = exe_path.parent_path() / "dropshell.old";
|
|
if (std::filesystem::exists(backup_path))
|
|
{
|
|
std::filesystem::remove(backup_path);
|
|
}
|
|
|
|
std::filesystem::rename(exe_path, backup_path);
|
|
std::filesystem::rename(temp_file, exe_path);
|
|
|
|
// Remove the backup after successful replacement
|
|
std::filesystem::remove(backup_path);
|
|
|
|
info << "Update complete. Restarting dropshell..." << std::endl;
|
|
|
|
// Execute the new version
|
|
execlp("bash", "bash", "-c", (exe_path.string() + " install").c_str(), (char *)nullptr);
|
|
error << "Failed to execute new version of dropshell." << std::endl;
|
|
return -1;
|
|
}
|
|
|
|
int install_local_agent()
|
|
{
|
|
maketitle("Installing dropshell agent on this computer...");
|
|
|
|
// clear out old cruft.
|
|
std::filesystem::remove_all(localpath::agent_local());
|
|
std::filesystem::remove_all(localpath::agent_remote());
|
|
|
|
// recreate the directories.
|
|
localpath::create_directories();
|
|
|
|
// populate the agent-local directory.
|
|
recreate_agent_local::recreate_tree(localpath::agent_local());
|
|
|
|
// run the local agent installer.
|
|
execute_local_command(localpath::agent_local(), "agent-install.sh",{}, nullptr, cMode::Defaults | cMode::NoBB64);
|
|
|
|
// populate the agent-remote directory.
|
|
info << "Creating local files to copy to remote agents..." << std::endl;
|
|
recreate_agent_remote::recreate_tree(localpath::agent_remote());
|
|
|
|
return 0;
|
|
}
|
|
|
|
int install_server(const ServerConfig &server)
|
|
{
|
|
// install the dropshell agent on the given server.
|
|
maketitle("Installing dropshell agent on " + server.get_server_name(), sColour::INFO);
|
|
|
|
for (const auto &user : server.get_users())
|
|
{
|
|
info << "Installing agent for user " << user.user << " on " << server.get_server_name() << std::endl;
|
|
|
|
std::string agent_path = remotepath(server.get_server_name(),user.user).agent();
|
|
ASSERT(agent_path == user.dir+"/agent", "Remote agent path does not match user directory for "+user.user+"@" + server.get_server_name() + " : " + agent_path + " != " + user.dir);
|
|
ASSERT(!agent_path.empty(), "Agent path is empty for " + user.user + "@" + server.get_server_name());
|
|
|
|
// now create the agent.
|
|
// copy across from the local agent files.
|
|
info << "Copying local agent files to remote server... " << std::flush;
|
|
shared_commands::rsync_tree_to_remote(localpath::agent_remote(), agent_path, server, false, user.user);
|
|
info << "done." << std::endl;
|
|
|
|
// run the agent installer. Can't use BB64 yet, as we're installing it on the remote server.
|
|
|
|
bool okay = execute_ssh_command(server.get_SSH_INFO(user.user), sCommand(agent_path, "agent-install.sh",{}), cMode::Defaults | cMode::NoBB64, nullptr);
|
|
if (!okay)
|
|
{
|
|
error << "Failed to install remote agent on " << server.get_server_name() << std::endl;
|
|
return 1;
|
|
}
|
|
|
|
info << "Installation on " << server.get_server_name() << " complete." << std::endl;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// ------------------------------------------------------------------------------------------------
|
|
// install_host
|
|
// ------------------------------------------------------------------------------------------------
|
|
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;
|
|
|
|
// install the dropshell agent on all servers.
|
|
std::vector<ServerConfig> servers = get_configured_servers();
|
|
for (const auto &server : servers)
|
|
{
|
|
rval = install_server(server);
|
|
if (rval != 0)
|
|
return rval;
|
|
}
|
|
|
|
std::cout << "Installation complete." << std::endl;
|
|
return 0;
|
|
}
|
|
|
|
// ------------------------------------------------------------------------------------------------
|
|
// install command implementation
|
|
// ------------------------------------------------------------------------------------------------
|
|
int install_handler(const CommandContext &ctx)
|
|
{
|
|
if (ctx.args.size() < 1)
|
|
{ // install host
|
|
return install_host();
|
|
}
|
|
|
|
if (!gConfig().is_config_set())
|
|
{
|
|
error << "Dropshell is not configured. Please run 'dropshell edit' to configure it." << std::endl;
|
|
return 1;
|
|
}
|
|
|
|
std::string server = safearg(ctx.args, 0);
|
|
|
|
if (ctx.args.size() == 1)
|
|
{ // install server
|
|
return install_server(server);
|
|
}
|
|
|
|
// install service(s)
|
|
if (!server_exists(server))
|
|
{
|
|
error << "Server " << server << " does not exist." << std::endl;
|
|
info << "Create it with: dropshell create-server " << server << std::endl;
|
|
return 1;
|
|
}
|
|
|
|
ServerConfig server_env(server);
|
|
ASSERT(server_env.is_valid(), "Invalid server environment for " + server);
|
|
if (safearg(ctx.args, 1) == "all")
|
|
{
|
|
// install all services on the server
|
|
maketitle("Installing all services on " + server);
|
|
bool okay = true;
|
|
std::vector<LocalServiceInfo> services = get_server_services_info(server);
|
|
for (const auto &lsi : services)
|
|
{
|
|
if (!shared_commands::install_service(server_env, lsi.service_name))
|
|
okay = false;
|
|
}
|
|
return okay ? 0 : 1;
|
|
}
|
|
else
|
|
{ // install the specific service.
|
|
std::string service = safearg(ctx.args, 1);
|
|
return shared_commands::install_service(server_env, service) ? 0 : 1;
|
|
}
|
|
}
|
|
|
|
} // namespace dropshell
|