Add check-updates command to detect and update outdated agents and services
All checks were successful
Build-Test-Publish / build (linux/amd64) (push) Successful in 48s
Build-Test-Publish / build (linux/arm64) (push) Successful in 3m19s

This commit is contained in:
j
2026-03-28 11:56:11 +13:00
parent bea008d153
commit dafc0529f0
6 changed files with 271 additions and 1 deletions

View File

@@ -95,6 +95,12 @@ if [[ -d "${SERVICES_DIR}" ]]; then
ports="$ports_output" ports="$ports_output"
fi 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 -- # -- Output JSON for this service --
if [[ "$first" == "true" ]]; then if [[ "$first" == "true" ]]; then
first=false first=false
@@ -108,6 +114,8 @@ if [[ -d "${SERVICES_DIR}" ]]; then
json_escape "$status" json_escape "$status"
echo -n '","ports":"' echo -n '","ports":"'
json_escape "$ports" json_escape "$ports"
echo -n '","template_hash":"'
json_escape "$template_hash"
echo -n '"}' echo -n '"}'
done done
fi fi

View File

@@ -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 <iostream>
#include <fstream>
#include <mutex>
#include <set>
namespace dropshell {
// Declared in install.cpp
int install_server(const ServerConfig& server);
int check_updates_handler(const CommandContext& ctx);
static std::vector<std::string> 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<std::string> 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<ServerUserPair> pairs;
for (const auto& server : servers)
for (const auto& user : server.get_users())
pairs.push_back({server, user});
// Results
std::vector<OutdatedItem> 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<std::string, shared_commands::ServiceStatus> status =
shared_commands::get_all_services_status(server_env, sup.user.user, &agent_match);
std::lock_guard<std::mutex> 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<LocalServiceInfo> 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<OutdatedItem> outdated_agents;
std::vector<OutdatedItem> 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<std::string> 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

View File

@@ -140,6 +140,7 @@ int help_handler(const CommandContext& ctx) {
show_command("list"); show_command("list");
info << std::endl; info << std::endl;
show_command("install"); show_command("install");
show_command("check-updates");
show_command("uninstall"); show_command("uninstall");
show_command("destroy"); show_command("destroy");
info << std::endl; info << std::endl;

View File

@@ -4,7 +4,7 @@
#include "utils/directories.hpp" #include "utils/directories.hpp"
#include "templates.hpp" #include "templates.hpp"
#include "shared_commands.hpp" #include "shared_commands.hpp"
//#include "utils/hash.hpp" #include "utils/hash.hpp"
#include "autogen/_agent-local.hpp" #include "autogen/_agent-local.hpp"
#include "autogen/_agent-remote.hpp" #include "autogen/_agent-remote.hpp"
#include "services.hpp" #include "services.hpp"
@@ -197,6 +197,26 @@ namespace dropshell
return false; 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 // print health tick
info << "Health: " << shared_commands::healthtick(server, service) << std::endl; info << "Health: " << shared_commands::healthtick(server, service) << std::endl;

View File

@@ -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<std::string>();
}
status[service_name] = service_status; status[service_name] = service_status;
} }
} catch (const nlohmann::json::parse_error& e) { } catch (const nlohmann::json::parse_error& e) {

View File

@@ -26,6 +26,7 @@ namespace dropshell
{ {
HealthStatus health; HealthStatus health;
std::vector<int> ports; std::vector<int> ports;
std::string remote_template_hash;
} ServiceStatus; } ServiceStatus;
// expose routines used by multiple commands. // expose routines used by multiple commands.