Add info-json command for server hardware inventory
This commit is contained in:
@@ -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 '"'
|
||||
|
||||
186
source/src/commands/info_json.cpp
Normal file
186
source/src/commands/info_json.cpp
Normal file
@@ -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 <nlohmann/json.hpp>
|
||||
#include <libassert/assert.hpp>
|
||||
#include <mutex>
|
||||
#include <set>
|
||||
#include <regex>
|
||||
|
||||
namespace dropshell {
|
||||
|
||||
int info_json_handler(const CommandContext &ctx);
|
||||
|
||||
static std::vector<std::string> 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<std::string> disabled_servers;
|
||||
for (const auto &d : gConfig().get_disabled_servers())
|
||||
disabled_servers.insert(d);
|
||||
|
||||
struct ServerEntry {
|
||||
ServerConfig server;
|
||||
std::string user;
|
||||
};
|
||||
std::vector<ServerEntry> 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<std::string>()); } 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<std::string>()) / 1024.0; } catch (...) {}
|
||||
}
|
||||
// Round to 1 decimal
|
||||
server_json["memory_gb"] = static_cast<int>(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<std::string>()); } 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<std::string>()); } 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<std::string>()); } 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<std::string>()));
|
||||
}
|
||||
}
|
||||
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<std::mutex> 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
|
||||
Reference in New Issue
Block a user