diff --git a/source/agent-remote/hostinfo.sh b/source/agent-remote/hostinfo.sh index 25e78a4..e3e3af2 100755 --- a/source/agent-remote/hostinfo.sh +++ b/source/agent-remote/hostinfo.sh @@ -34,6 +34,9 @@ RAM_USED_MB=$(( RAM_TOTAL_MB - RAM_AVAIL_MB )) DISK_ROOT_USED_GB=$(df -BG / 2>/dev/null | awk 'NR==2{gsub(/G/,"",$3); print $3}' || echo "0") DISK_ROOT_TOTAL_GB=$(df -BG / 2>/dev/null | awk 'NR==2{gsub(/G/,"",$2); print $2}' || echo "0") +# Largest disk by total capacity (across all mounted filesystems) +DISK_LARGEST_TOTAL_GB=$(df -BG 2>/dev/null | awk 'NR>1{gsub(/G/,"",$2); if($2+0 > max) max=$2+0} END{print max+0}') + # Disk /tank (only if mounted) DISK_TANK_USED_GB="" DISK_TANK_TOTAL_GB="" @@ -101,6 +104,7 @@ echo -n ',"ram_used_mb":"'; json_escape "$RAM_USED_MB"; echo -n '"' echo -n ',"ram_total_mb":"'; json_escape "$RAM_TOTAL_MB"; echo -n '"' echo -n ',"disk_root_used_gb":"'; json_escape "$DISK_ROOT_USED_GB"; echo -n '"' echo -n ',"disk_root_total_gb":"'; json_escape "$DISK_ROOT_TOTAL_GB"; echo -n '"' +echo -n ',"disk_largest_total_gb":"'; json_escape "$DISK_LARGEST_TOTAL_GB"; echo -n '"' echo -n ',"disk_tank_used_gb":"'; json_escape "$DISK_TANK_USED_GB"; echo -n '"' echo -n ',"disk_tank_total_gb":"'; json_escape "$DISK_TANK_TOTAL_GB"; echo -n '"' echo -n ',"ip_local":"'; json_escape "$IP_LOCAL"; echo -n '"' diff --git a/source/src/commands/info_json.cpp b/source/src/commands/info_json.cpp new file mode 100644 index 0000000..a12e0ee --- /dev/null +++ b/source/src/commands/info_json.cpp @@ -0,0 +1,186 @@ +#include "command_registry.hpp" +#include "config.hpp" +#include "servers.hpp" +#include "utils/output.hpp" +#include "utils/execute.hpp" +#include "utils/directories.hpp" +#include "transwarp.hpp" + +#include +#include +#include +#include +#include + +namespace dropshell { + +int info_json_handler(const CommandContext &ctx); + +static std::vector info_json_name_list = {"info-json"}; + +struct InfoJsonCommandRegister { + InfoJsonCommandRegister() { + CommandRegistry::instance().register_command({ + info_json_name_list, + info_json_handler, + nullptr, + false, // hidden + true, // requires_config + true, // requires_install + 0, // min_args + 0, // max_args + "info-json", + "Output JSON with hardware info for all servers.", + R"( +Output JSON array with hardware info for all configured servers. + info-json Query all servers and return JSON with server name, SSH address, + CPU cores, memory, largest disk capacity, and GPU names. + )" + }); + } +} info_json_command_register; + +// Extract nice GPU name from lspci output. +// e.g. "NVIDIA Corporation GA102 [GeForce RTX 3090]" -> "GeForce RTX 3090" +static std::string gpu_nice_name(const std::string &raw) { + // Find the last bracketed section + auto last_open = raw.rfind('['); + auto last_close = raw.rfind(']'); + if (last_open != std::string::npos && last_close != std::string::npos && last_close > last_open + 1) { + return raw.substr(last_open + 1, last_close - last_open - 1); + } + // No brackets - strip common vendor prefixes + std::string name = raw; + for (const char *prefix : {"NVIDIA Corporation ", "Advanced Micro Devices, Inc. ", "Intel Corporation "}) { + if (name.find(prefix) == 0) { + name = name.substr(strlen(prefix)); + break; + } + } + return name; +} + +int info_json_handler(const CommandContext &ctx) { + auto servers = get_configured_servers(); + if (servers.empty()) { + rawout << "[]" << std::endl; + return 0; + } + + // Skip disabled servers + std::set disabled_servers; + for (const auto &d : gConfig().get_disabled_servers()) + disabled_servers.insert(d); + + struct ServerEntry { + ServerConfig server; + std::string user; + }; + std::vector entries; + for (const auto &server : servers) { + if (disabled_servers.count(server.get_server_name())) + continue; + if (server.get_users().empty()) + continue; + entries.push_back({server, server.get_users()[0].user}); + } + + nlohmann::json result = nlohmann::json::array(); + std::mutex result_mutex; + + info << "Querying " << entries.size() << " servers: " << std::flush; + int checked = 0; + + transwarp::parallel exec{entries.size()}; + auto task = transwarp::for_each(exec, entries.begin(), entries.end(), [&](const ServerEntry &entry) { + std::string server_name = entry.server.get_server_name(); + std::string ssh_host = entry.server.get_SSH_HOST(); + std::string ssh_user = entry.user; + + std::string agent_path = remotepath(server_name, ssh_user).agent(); + std::string script_path = agent_path + "/hostinfo.sh"; + + std::string output; + sCommand cmd(agent_path, script_path, {}); + bool success = execute_ssh_command(entry.server.get_SSH_INFO(ssh_user), cmd, cMode::Silent, &output); + + nlohmann::json server_json; + server_json["server_name"] = server_name; + server_json["ssh"] = ssh_user + "@" + ssh_host; + + if (!success || output.empty()) { + server_json["error"] = "unreachable"; + server_json["cpu_cores"] = 0; + server_json["memory_gb"] = 0; + server_json["disk_capacity_gb"] = 0; + server_json["gpus"] = nlohmann::json::array(); + } else { + try { + nlohmann::json j = nlohmann::json::parse(output); + + // CPU cores + int cores = 0; + if (j.contains("cpu_cores") && j["cpu_cores"].is_string()) { + try { cores = std::stoi(j["cpu_cores"].get()); } catch (...) {} + } + server_json["cpu_cores"] = cores; + + // Memory (total MB -> GB, rounded) + double memory_gb = 0; + if (j.contains("ram_total_mb") && j["ram_total_mb"].is_string()) { + try { memory_gb = std::stoi(j["ram_total_mb"].get()) / 1024.0; } catch (...) {} + } + // Round to 1 decimal + server_json["memory_gb"] = static_cast(memory_gb + 0.5); + + // Largest disk capacity - prefer new field, fall back to max of root/tank + int disk_gb = 0; + if (j.contains("disk_largest_total_gb") && j["disk_largest_total_gb"].is_string()) { + try { disk_gb = std::stoi(j["disk_largest_total_gb"].get()); } catch (...) {} + } + if (disk_gb == 0) { + int root_gb = 0, tank_gb = 0; + if (j.contains("disk_root_total_gb") && j["disk_root_total_gb"].is_string()) { + try { root_gb = std::stoi(j["disk_root_total_gb"].get()); } catch (...) {} + } + if (j.contains("disk_tank_total_gb") && j["disk_tank_total_gb"].is_string()) { + try { tank_gb = std::stoi(j["disk_tank_total_gb"].get()); } catch (...) {} + } + disk_gb = std::max(root_gb, tank_gb); + } + server_json["disk_capacity_gb"] = disk_gb; + + // GPUs with nice names + nlohmann::json gpus = nlohmann::json::array(); + if (j.contains("gpu") && j["gpu"].is_array()) { + for (const auto &g : j["gpu"]) { + if (g.is_string()) + gpus.push_back(gpu_nice_name(g.get())); + } + } + server_json["gpus"] = gpus; + + } catch (const nlohmann::json::parse_error &) { + server_json["error"] = "parse_error"; + server_json["cpu_cores"] = 0; + server_json["memory_gb"] = 0; + server_json["disk_capacity_gb"] = 0; + server_json["gpus"] = nlohmann::json::array(); + } + } + + { + std::lock_guard lock(result_mutex); + result.push_back(server_json); + ++checked; + } + info << checked << " " << std::flush; + }); + task->wait(); + info << std::endl; + + rawout << result.dump(2) << std::endl; + return 0; +} + +} // namespace dropshell