diff --git a/TEMPLATES.md b/TEMPLATES.md index de4db2d..4a50932 100644 --- a/TEMPLATES.md +++ b/TEMPLATES.md @@ -168,7 +168,7 @@ _check_required_env_vars "CONTAINER_NAME" "IMAGE_REGISTRY" "IMAGE_REPO" "IMAGE_T _check_docker_installed || _die "Docker test failed" # Pull the Docker image -docker pull "$IMAGE_REGISTRY/$IMAGE_REPO:$IMAGE_TAG" || _die "Failed to pull image" +docker pull -q "$IMAGE_REGISTRY/$IMAGE_REPO:$IMAGE_TAG" || _die "Failed to pull image" # Stop any existing container bash ./stop.sh || _die "Failed to stop container" diff --git a/source/agent-remote/hostinfo.sh b/source/agent-remote/hostinfo.sh new file mode 100755 index 0000000..2058481 --- /dev/null +++ b/source/agent-remote/hostinfo.sh @@ -0,0 +1,113 @@ +#!/bin/bash +set -uo pipefail + +# hostinfo.sh - Gather hardware/system info from a remote host +# +# Output: JSON object with system information + +# -- Helper to escape JSON strings -- +json_escape() { + local str="$1" + str="${str//\\/\\\\}" + str="${str//\"/\\\"}" + str="${str//$'\n'/\\n}" + str="${str//$'\r'/\\r}" + str="${str//$'\t'/\\t}" + echo -n "$str" +} + +# -- Gather system info -- + +HOSTNAME=$(hostname 2>/dev/null || echo "Unknown") + +CPU=$(grep -m1 'model name' /proc/cpuinfo 2>/dev/null | cut -d: -f2 | xargs || echo "Unknown") +CPU_CORES=$(nproc 2>/dev/null || grep -c '^processor' /proc/cpuinfo 2>/dev/null || echo "0") + +MOTHERBOARD=$(cat /sys/devices/virtual/dmi/id/board_name 2>/dev/null || echo "Unknown") + +# RAM from free -m (used = total - available) +RAM_TOTAL_MB=$(free -m 2>/dev/null | awk 'NR==2{print $2}' || echo "0") +RAM_AVAIL_MB=$(free -m 2>/dev/null | awk 'NR==2{print $7}' || echo "0") +RAM_USED_MB=$(( RAM_TOTAL_MB - RAM_AVAIL_MB )) + +# Disk root (strip trailing G from df -BG output) +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 /tank (only if mounted) +DISK_TANK_USED_GB="" +DISK_TANK_TOTAL_GB="" +if [ -d /tank ] && mountpoint -q /tank 2>/dev/null; then + DISK_TANK_USED_GB=$(df -BG /tank 2>/dev/null | awk 'NR==2{gsub(/G/,"",$3); print $3}' || echo "") + DISK_TANK_TOTAL_GB=$(df -BG /tank 2>/dev/null | awk 'NR==2{gsub(/G/,"",$2); print $2}' || echo "") +fi + +# IP addresses +IP_LOCAL=$(hostname -I 2>/dev/null | awk '{print $1}' || echo "Unknown") + +IP_TAILSCALE="" +if command -v tailscale &>/dev/null; then + IP_TAILSCALE=$(tailscale ip -4 2>/dev/null || echo "") +fi + +IP_PUBLIC="" +if command -v curl &>/dev/null; then + IP_PUBLIC=$(curl -s --max-time 3 ifconfig.me 2>/dev/null || echo "") +elif command -v wget &>/dev/null; then + IP_PUBLIC=$(wget -qO- --timeout=3 ifconfig.me 2>/dev/null || echo "") +fi + +# OS info +OS=$(grep PRETTY_NAME /etc/os-release 2>/dev/null | cut -d= -f2 | tr -d '"' || uname -o 2>/dev/null || echo "Unknown") +KERNEL=$(uname -r 2>/dev/null || echo "Unknown") +UPTIME=$(uptime -p 2>/dev/null || uptime 2>/dev/null || echo "Unknown") + +# Docker +DOCKER_VERSION="" +if command -v docker &>/dev/null; then + DOCKER_VERSION=$(docker --version 2>/dev/null | awk '{print $3}' | tr -d ',' || echo "") +fi + +# GPU list (may have 0, 1, or multiple) +GPU_JSON="[]" +if command -v lspci &>/dev/null; then + GPU_LINES=$(lspci 2>/dev/null | grep -i 'vga\|3d\|display' | sed 's/^[^ ]* //' || true) + if [ -n "$GPU_LINES" ]; then + GPU_JSON="[" + first=true + while IFS= read -r line; do + [ -z "$line" ] && continue + if [ "$first" = "true" ]; then + first=false + else + GPU_JSON+="," + fi + GPU_JSON+="\"" + GPU_JSON+=$(json_escape "$line") + GPU_JSON+="\"" + done <<< "$GPU_LINES" + GPU_JSON+="]" + fi +fi + +# -- Output JSON -- +echo -n '{' +echo -n '"hostname":"'; json_escape "$HOSTNAME"; echo -n '"' +echo -n ',"cpu":"'; json_escape "$CPU"; echo -n '"' +echo -n ',"cpu_cores":"'; json_escape "$CPU_CORES"; echo -n '"' +echo -n ',"motherboard":"'; json_escape "$MOTHERBOARD"; echo -n '"' +echo -n ',"gpu":'"$GPU_JSON" +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_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 '"' +echo -n ',"ip_tailscale":"'; json_escape "$IP_TAILSCALE"; echo -n '"' +echo -n ',"ip_public":"'; json_escape "$IP_PUBLIC"; echo -n '"' +echo -n ',"os":"'; json_escape "$OS"; echo -n '"' +echo -n ',"kernel":"'; json_escape "$KERNEL"; echo -n '"' +echo -n ',"uptime":"'; json_escape "$UPTIME"; echo -n '"' +echo -n ',"docker_version":"'; json_escape "$DOCKER_VERSION"; echo -n '"' +echo '}' diff --git a/source/src/commands/hostinfo.cpp b/source/src/commands/hostinfo.cpp new file mode 100644 index 0000000..09f6955 --- /dev/null +++ b/source/src/commands/hostinfo.cpp @@ -0,0 +1,171 @@ +#include "command_registry.hpp" +#include "servers.hpp" +#include "utils/output.hpp" +#include "utils/execute.hpp" +#include "utils/directories.hpp" +#include "shared_commands.hpp" +#include "tableprint.hpp" + +#include +#include +#include + +namespace dropshell { + +int hostinfo_handler(const CommandContext &ctx); + +static std::vector hostinfo_name_list = {"hostinfo", "sysinfo"}; + +void hostinfo_autocomplete(const CommandContext &ctx) { + if (ctx.args.size() == 0) { + std::vector servers = get_configured_servers(); + for (const auto &server : servers) + rawout << server.get_server_name() << std::endl; + } +} + +struct HostinfoCommandRegister { + HostinfoCommandRegister() { + CommandRegistry::instance().register_command({ + hostinfo_name_list, + hostinfo_handler, + hostinfo_autocomplete, + false, // hidden + true, // requires_config + true, // requires_install + 1, // min_args + 1, // max_args + "hostinfo SERVER", + "Display hardware and system information for a remote server.", + R"( +Display hardware and system information for a remote server. + hostinfo SERVER Show CPU, motherboard, GPU, RAM, disk usage, + IP addresses, OS, kernel, uptime, and Docker version. + )" + }); + } +} hostinfo_command_register; + +int hostinfo_handler(const CommandContext &ctx) { + std::string server_name = ctx.args[0]; + + ServerConfig server_env(server_name); + if (!server_env.is_valid()) { + error << "Server '" << server_name << "' is not valid." << std::endl; + return 1; + } + + ASSERT(server_env.get_users().size() > 0, "No users found for server " + server_name); + std::string user = server_env.get_users()[0].user; + + std::string agent_path = remotepath(server_name, user).agent(); + std::string script_path = agent_path + "/hostinfo.sh"; + + std::string output; + sCommand cmd(agent_path, script_path, {}); + bool success = execute_ssh_command(server_env.get_SSH_INFO(user), cmd, cMode::Silent, &output); + + if (!success || output.empty()) { + error << "Failed to retrieve host information from " << server_name << std::endl; + return 1; + } + + nlohmann::json j; + try { + j = nlohmann::json::parse(output); + } catch (const nlohmann::json::parse_error &e) { + error << "Failed to parse host info: " << e.what() << std::endl; + debug << "Output was: " << output << std::endl; + return 1; + } + + auto get_str = [&](const std::string &key) -> std::string { + if (j.contains(key) && j[key].is_string()) { + std::string val = j[key].get(); + return val.empty() ? "-" : val; + } + return "-"; + }; + + // Format RAM as "X.X / Y.Y GB" + std::string ram_display = "-"; + { + std::string used = get_str("ram_used_mb"); + std::string total = get_str("ram_total_mb"); + if (used != "-" && total != "-") { + try { + int used_mb = std::stoi(used); + int total_mb = std::stoi(total); + char buf[64]; + snprintf(buf, sizeof(buf), "%.1f / %.1f GB", + used_mb / 1024.0, total_mb / 1024.0); + ram_display = buf; + } catch (...) {} + } + } + + // Format disk root + std::string disk_root_display = "-"; + { + std::string used = get_str("disk_root_used_gb"); + std::string total = get_str("disk_root_total_gb"); + if (used != "-" && total != "-") + disk_root_display = used + " / " + total + " GB"; + } + + // Format disk /tank (only if present) + std::string disk_tank_display; + { + std::string used = get_str("disk_tank_used_gb"); + std::string total = get_str("disk_tank_total_gb"); + if (used != "-" && total != "-") + disk_tank_display = used + " / " + total + " GB"; + } + + // Format GPU list + std::string gpu_display = "-"; + if (j.contains("gpu") && j["gpu"].is_array() && !j["gpu"].empty()) { + gpu_display.clear(); + for (size_t i = 0; i < j["gpu"].size(); ++i) { + if (i > 0) gpu_display += ", "; + gpu_display += j["gpu"][i].get(); + } + } + + // Format CPU with cores + std::string cpu_display = get_str("cpu"); + std::string cores = get_str("cpu_cores"); + if (cores != "-") + cpu_display += " (" + cores + " cores)"; + + // Format IP addresses on one line + std::string ip_display = get_str("ip_local"); + { + std::string ts = get_str("ip_tailscale"); + std::string pub = get_str("ip_public"); + if (ts != "-") ip_display += " ts:" + ts; + if (pub != "-") ip_display += " pub:" + pub; + } + + // Display + tableprint tp("Host Info: " + server_name, true); + tp.add_row({"Property", "Value"}); + tp.add_row({"Hostname", get_str("hostname")}); + tp.add_row({"OS", get_str("os")}); + tp.add_row({"Kernel", get_str("kernel")}); + tp.add_row({"Uptime", get_str("uptime")}); + tp.add_row({"CPU", cpu_display}); + tp.add_row({"Motherboard", get_str("motherboard")}); + tp.add_row({"GPU", gpu_display}); + tp.add_row({"RAM", ram_display}); + tp.add_row({"Disk /", disk_root_display}); + if (!disk_tank_display.empty()) + tp.add_row({"Disk /tank", disk_tank_display}); + tp.add_row({"IP Addresses", ip_display}); + tp.add_row({"Docker", get_str("docker_version")}); + tp.print(); + + return 0; +} + +} // namespace dropshell diff --git a/source/src/templates.cpp b/source/src/templates.cpp index e8dbb83..7df0665 100644 --- a/source/src/templates.cpp +++ b/source/src/templates.cpp @@ -561,7 +561,7 @@ _check_required_env_vars "CONTAINER_NAME" "IMAGE_REGISTRY" "IMAGE_REPO" "IMAGE_T _check_docker_installed || _die "Docker test failed" # Pull the Docker image -docker pull "$IMAGE_REGISTRY/$IMAGE_REPO:$IMAGE_TAG" || _die "Failed to pull image" +docker pull -q "$IMAGE_REGISTRY/$IMAGE_REPO:$IMAGE_TAG" || _die "Failed to pull image" # Stop any existing container bash "$SCRIPT_DIR/stop.sh" 2>/dev/null || true