diff --git a/source/agent-remote/all_status.sh b/source/agent-remote/all_status.sh index 1188790..5793457 100755 --- a/source/agent-remote/all_status.sh +++ b/source/agent-remote/all_status.sh @@ -95,6 +95,12 @@ if [[ -d "${SERVICES_DIR}" ]]; then ports="$ports_output" fi + # -- Get template hash -- + template_hash="" + if [[ -f "${service_dir}template.hash" ]]; then + template_hash=$(cat "${service_dir}template.hash" | tr -d '[:space:]') + fi + # -- Output JSON for this service -- if [[ "$first" == "true" ]]; then first=false @@ -108,6 +114,8 @@ if [[ -d "${SERVICES_DIR}" ]]; then json_escape "$status" echo -n '","ports":"' json_escape "$ports" + echo -n '","template_hash":"' + json_escape "$template_hash" echo -n '"}' done fi diff --git a/source/src/commands/check-updates.cpp b/source/src/commands/check-updates.cpp new file mode 100644 index 0000000..7161cd4 --- /dev/null +++ b/source/src/commands/check-updates.cpp @@ -0,0 +1,235 @@ +#include "command_registry.hpp" +#include "config.hpp" +#include "utils/utils.hpp" +#include "utils/directories.hpp" +#include "utils/hash.hpp" +#include "shared_commands.hpp" +#include "servers.hpp" +#include "services.hpp" +#include "templates.hpp" +#include "tableprint.hpp" +#include "transwarp.hpp" + +#include +#include +#include +#include + +namespace dropshell { + +// Declared in install.cpp +int install_server(const ServerConfig& server); + +int check_updates_handler(const CommandContext& ctx); + +static std::vector check_updates_name_list = {"check-updates", "outdated"}; + +struct CheckUpdatesCommandRegister { + CheckUpdatesCommandRegister() { + CommandRegistry::instance().register_command({ + check_updates_name_list, + check_updates_handler, + nullptr, // no autocomplete + false, // hidden + true, // requires_config + true, // requires_install + 0, // min_args + 0, // max_args + "check-updates", + "Check for outdated agents and services, offer to update", + R"( +Check all servers for outdated remote agents and services with +changed templates. Displays a summary of what needs updating and +offers to update everything. + + check-updates Check all servers and services. + outdated Alias for check-updates. + )" + }); + } +} check_updates_command_register; + +struct OutdatedItem { + std::string server_name; + std::string user; + std::string service_name; + std::string template_name; + enum class Kind { Agent, Service } kind; +}; + +int check_updates_handler(const CommandContext& ctx) { + // Pull latest templates from git first + info << "Pulling latest templates..." << std::endl; + gTemplateManager().pull_all(); + + auto servers = get_configured_servers(); + if (servers.empty()) { + info << "No servers configured." << std::endl; + return 0; + } + + // Pre-compute disabled servers + std::set disabled_servers; + for (const auto& s : gConfig().get_disabled_servers()) + disabled_servers.insert(s); + + // Build list of server+user pairs + struct ServerUserPair { ServerConfig server; UserConfig user; }; + std::vector pairs; + for (const auto& server : servers) + for (const auto& user : server.get_users()) + pairs.push_back({server, user}); + + // Results + std::vector outdated_items; + std::mutex results_mutex; + int checked = 0; + + info << "Checking " << pairs.size() << " agents: " << std::flush; + + transwarp::parallel exec{pairs.size()}; + auto task = transwarp::for_each(exec, pairs.begin(), pairs.end(), [&](const ServerUserPair& sup) { + std::string server_name = sup.server.get_server_name(); + + if (disabled_servers.count(server_name)) + return; + + ServerConfig server_env(server_name); + if (!server_env.is_valid()) + return; + + // Check if server is online + sSSHInfo sshinfo = server_env.get_SSH_INFO(sup.user.user); + if (!execute_ssh_command(sshinfo, sCommand("", "true", {}), cMode::Silent)) + return; + + // Get all service statuses + agent match + bool agent_match = true; + std::map status = + shared_commands::get_all_services_status(server_env, sup.user.user, &agent_match); + + std::lock_guard lock(results_mutex); + + // Check agent + if (!agent_match) { + outdated_items.push_back({server_name, sup.user.user, "", "", OutdatedItem::Kind::Agent}); + } + + // Check each service's template hash + std::vector services = get_server_services_info(server_name); + for (const auto& svc : services) { + if (svc.user != sup.user.user) + continue; + + // Compute current local template hash + template_info tinfo = gTemplateManager().get_template_info(svc.template_name); + if (!tinfo.is_set()) + continue; + + std::string local_hash = hash_directory_recursive(tinfo.local_template_path().string()); + if (local_hash.empty()) + continue; + + // Get remote hash from status response + auto it = status.find(svc.service_name); + std::string remote_hash; + if (it != status.end()) + remote_hash = it->second.remote_template_hash; + + // If remote hash is empty, the service was installed before hash tracking - treat as outdated + // If hashes differ, template has changed since last install + if (remote_hash.empty() || remote_hash != local_hash) { + outdated_items.push_back({server_name, svc.user, svc.service_name, svc.template_name, OutdatedItem::Kind::Service}); + } + } + + ++checked; + info << checked << " " << std::flush; + }); + task->wait(); + info << std::endl << std::endl; + + if (outdated_items.empty()) { + info << "Everything is up to date!" << std::endl; + return 0; + } + + // Display results + tableprint tp("Outdated Items"); + tp.add_row({"Server", "User", "Type", "Details"}); + + // Separate agents and services for ordered display + std::vector outdated_agents; + std::vector outdated_services; + for (const auto& item : outdated_items) { + if (item.kind == OutdatedItem::Kind::Agent) + outdated_agents.push_back(item); + else + outdated_services.push_back(item); + } + + for (const auto& item : outdated_agents) + tp.add_row({item.server_name, item.user, "Agent", "Remote agent out of date"}); + + for (const auto& item : outdated_services) + tp.add_row({item.server_name, item.user, "Service", item.service_name + " (" + item.template_name + ")"}); + + tp.print(); + + info << std::endl; + info << outdated_agents.size() << " outdated agent(s), " + << outdated_services.size() << " outdated service(s)." << std::endl; + info << std::endl; + + // Offer to update + std::cout << "Update all? [y/N] " << std::flush; + std::string response; + std::getline(std::cin, response); + + if (response.empty() || (response[0] != 'y' && response[0] != 'Y')) { + info << "No changes made." << std::endl; + return 0; + } + + // Perform updates: agents first, then services + int failures = 0; + + // Deduplicate agent updates (one per server+user) + std::set updated_servers; + for (const auto& item : outdated_agents) { + if (updated_servers.count(item.server_name)) + continue; + updated_servers.insert(item.server_name); + + ServerConfig server_env(item.server_name); + if (!server_env.is_valid()) { + ++failures; + continue; + } + + maketitle("Updating agent on " + item.server_name, sColour::INFO); + + if (install_server(server_env) != 0) + ++failures; + } + + // Update outdated services + for (const auto& item : outdated_services) { + ServerConfig server_env(item.server_name); + if (!server_env.is_valid()) { + ++failures; + continue; + } + if (!shared_commands::install_service(server_env, item.service_name)) + ++failures; + } + + if (failures == 0) + info << "All updates completed successfully." << std::endl; + else + warning << failures << " update(s) failed." << std::endl; + + return failures > 0 ? 1 : 0; +} + +} // namespace dropshell diff --git a/source/src/commands/help.cpp b/source/src/commands/help.cpp index a4c4389..08c4502 100644 --- a/source/src/commands/help.cpp +++ b/source/src/commands/help.cpp @@ -140,6 +140,7 @@ int help_handler(const CommandContext& ctx) { show_command("list"); info << std::endl; show_command("install"); + show_command("check-updates"); show_command("uninstall"); show_command("destroy"); info << std::endl; diff --git a/source/src/commands/install.cpp b/source/src/commands/install.cpp index b10c255..1716739 100644 --- a/source/src/commands/install.cpp +++ b/source/src/commands/install.cpp @@ -4,7 +4,7 @@ #include "utils/directories.hpp" #include "templates.hpp" #include "shared_commands.hpp" -//#include "utils/hash.hpp" +#include "utils/hash.hpp" #include "autogen/_agent-local.hpp" #include "autogen/_agent-remote.hpp" #include "services.hpp" @@ -197,6 +197,26 @@ namespace dropshell return false; } + // Record the template hash so we can detect outdated services later + { + std::string template_hash = hash_directory_recursive(tinfo.local_template_path().string()); + if (!template_hash.empty()) + { + // Write locally alongside service.env + std::string local_hash_file = service_info.local_service_path + "/.template_hash"; + std::ofstream hf(local_hash_file); + if (hf.is_open()) { + hf << template_hash; + hf.close(); + } + + // Write to remote service directory + std::string remote_hash_path = remote_service_path + "/template.hash"; + std::string write_hash_cmd = "echo -n " + quote(template_hash) + " > " + quote(remote_hash_path); + execute_ssh_command(server_env.get_SSH_INFO(user), sCommand("", write_hash_cmd, {}), cMode::Silent); + } + } + // print health tick info << "Health: " << shared_commands::healthtick(server, service) << std::endl; diff --git a/source/src/commands/shared_commands.cpp b/source/src/commands/shared_commands.cpp index cb14f5f..e1ff369 100644 --- a/source/src/commands/shared_commands.cpp +++ b/source/src/commands/shared_commands.cpp @@ -323,6 +323,11 @@ namespace dropshell } } + // Parse template hash + if (service_json.contains("template_hash") && service_json["template_hash"].is_string()) { + service_status.remote_template_hash = service_json["template_hash"].get(); + } + status[service_name] = service_status; } } catch (const nlohmann::json::parse_error& e) { diff --git a/source/src/commands/shared_commands.hpp b/source/src/commands/shared_commands.hpp index f9ac5ea..fff8823 100644 --- a/source/src/commands/shared_commands.hpp +++ b/source/src/commands/shared_commands.hpp @@ -26,6 +26,7 @@ namespace dropshell { HealthStatus health; std::vector ports; + std::string remote_template_hash; } ServiceStatus; // expose routines used by multiple commands.