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_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")
|
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 (only if mounted)
|
||||||
DISK_TANK_USED_GB=""
|
DISK_TANK_USED_GB=""
|
||||||
DISK_TANK_TOTAL_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 ',"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_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_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_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 ',"disk_tank_total_gb":"'; json_escape "$DISK_TANK_TOTAL_GB"; echo -n '"'
|
||||||
echo -n ',"ip_local":"'; json_escape "$IP_LOCAL"; 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