- protocol.h: add K_LOCATOR, K_SOCKET, K_DEVICE_NAME natural key constants - models.h: add locator/socket_designation/device_name fields to structs - database.cpp: apply_*, serialize_*, save_nolock, load all updated; upsert_part gains natural key fallback for memory/cpu/disk - discovery.cpp: emit K_LOCATOR for memory sticks/slots; fix CPU filter to skip phantom iLO entries; emit K_SOCKET for cpu/cpu_slot; emit K_DEVICE_NAME for disks with serial collision fix; replace NIC blocklist with /sys/class/net/<if>/device symlink check + lspci enrichment Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1196 lines
49 KiB
C++
1196 lines
49 KiB
C++
#include "client/discovery.h"
|
|
#include "common/models.h"
|
|
#include "common/protocol.h"
|
|
#include <algorithm>
|
|
#include <cerrno>
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <iostream>
|
|
#include <sstream>
|
|
#include <sys/stat.h>
|
|
|
|
// ═════════════════════════════════════════════════════════════════════════════
|
|
// Low-level helpers
|
|
// ═════════════════════════════════════════════════════════════════════════════
|
|
|
|
// Run a command with popen, return trimmed stdout (at most 64 KB).
|
|
std::string Discovery::run_cmd(const std::string& cmd) {
|
|
FILE* pipe = popen(cmd.c_str(), "r");
|
|
if (!pipe) return "";
|
|
|
|
std::string result;
|
|
char buf[4096];
|
|
while (fgets(buf, sizeof(buf), pipe)) {
|
|
result += buf;
|
|
if (result.size() >= 65536) break;
|
|
}
|
|
pclose(pipe);
|
|
|
|
// Trim trailing whitespace
|
|
while (!result.empty() && std::isspace((unsigned char)result.back()))
|
|
result.pop_back();
|
|
return result;
|
|
}
|
|
|
|
// Parse dmidecode -t <type_num> output into a vector of key→value maps.
|
|
// Each map corresponds to one DMI block (e.g. one "Memory Device").
|
|
std::vector<std::map<std::string, std::string>>
|
|
Discovery::parse_dmi(const std::string& type_num) {
|
|
#ifdef __APPLE__
|
|
(void)type_num;
|
|
return {}; // dmidecode not available on macOS; callers check __APPLE__
|
|
#else
|
|
std::string output = run_cmd("dmidecode -t " + type_num + " 2>/dev/null");
|
|
if (output.empty()) return {};
|
|
|
|
std::vector<std::map<std::string, std::string>> result;
|
|
std::map<std::string, std::string> current;
|
|
|
|
std::istringstream ss(output);
|
|
std::string line;
|
|
while (std::getline(ss, line)) {
|
|
if (!line.empty() && line.back() == '\r') line.pop_back();
|
|
|
|
// Handle empty — signals end of a block
|
|
if (line.empty()) {
|
|
if (!current.empty()) {
|
|
result.push_back(std::move(current));
|
|
current.clear();
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// "Handle 0x…" starts a new block
|
|
if (line.size() >= 6 && line.substr(0, 6) == "Handle") {
|
|
if (!current.empty()) {
|
|
result.push_back(std::move(current));
|
|
current.clear();
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// Tab-indented: " Key: Value" or just " Tag"
|
|
if (line[0] == '\t') {
|
|
std::string kv = line.substr(1);
|
|
size_t colon = kv.find(": ");
|
|
if (colon != std::string::npos) {
|
|
std::string k = kv.substr(0, colon);
|
|
std::string v = kv.substr(colon + 2);
|
|
while (!v.empty() && std::isspace((unsigned char)v.back()))
|
|
v.pop_back();
|
|
current[k] = v;
|
|
} else {
|
|
// Line with no colon is the block type header
|
|
while (!kv.empty() && std::isspace((unsigned char)kv.back()))
|
|
kv.pop_back();
|
|
if (!kv.empty()) current["__type__"] = kv;
|
|
}
|
|
} else if (line[0] != '#') {
|
|
// Non-indented, non-comment: block type header
|
|
while (!line.empty() && std::isspace((unsigned char)line.back()))
|
|
line.pop_back();
|
|
if (!line.empty()) current["__type__"] = line;
|
|
}
|
|
}
|
|
if (!current.empty()) result.push_back(std::move(current));
|
|
return result;
|
|
#endif
|
|
}
|
|
|
|
// ── Numeric / size helpers ────────────────────────────────────────────────────
|
|
namespace {
|
|
|
|
// "16384 MB", "16 GB", "No Module Installed", "" → MB as uint64_t
|
|
static uint64_t dmi_size_to_mb(const std::string& s) {
|
|
if (s.empty() || s.find("No Module") != std::string::npos) return 0;
|
|
size_t i = 0;
|
|
while (i < s.size() && (std::isdigit((unsigned char)s[i]) || s[i] == '.')) ++i;
|
|
if (i == 0) return 0;
|
|
double val = std::stod(s.substr(0, i));
|
|
std::string suffix = s.substr(i);
|
|
// trim leading whitespace from suffix
|
|
while (!suffix.empty() && std::isspace((unsigned char)suffix[0]))
|
|
suffix = suffix.substr(1);
|
|
if (suffix == "GB" || suffix == "GB" || suffix[0] == 'G') return static_cast<uint64_t>(val * 1024);
|
|
if (suffix == "TB" || suffix[0] == 'T') return static_cast<uint64_t>(val * 1024 * 1024);
|
|
return static_cast<uint64_t>(val); // assume MB
|
|
}
|
|
|
|
#ifndef __APPLE__
|
|
// "500G", "2T", "500M", "128K", raw bytes → GB as uint64_t
|
|
static uint64_t size_to_gb(const std::string& s) {
|
|
if (s.empty()) return 0;
|
|
size_t i = 0;
|
|
while (i < s.size() && (std::isdigit((unsigned char)s[i]) || s[i] == '.')) ++i;
|
|
if (i == 0) return 0;
|
|
double val = std::stod(s.substr(0, i));
|
|
std::string suffix = s.substr(i);
|
|
while (!suffix.empty() && std::isspace((unsigned char)suffix[0]))
|
|
suffix = suffix.substr(1);
|
|
char u = suffix.empty() ? '\0' : std::toupper((unsigned char)suffix[0]);
|
|
if (u == 'T') return static_cast<uint64_t>(val * 1024.0);
|
|
if (u == 'G') return static_cast<uint64_t>(val);
|
|
if (u == 'M') return static_cast<uint64_t>(val / 1024.0);
|
|
if (u == 'K') return static_cast<uint64_t>(val / (1024.0 * 1024.0));
|
|
// Assume raw bytes
|
|
return static_cast<uint64_t>(val / 1e9);
|
|
}
|
|
#endif // !__APPLE__
|
|
|
|
// Extract integer prefix from a string like "3600 MHz" → 3600
|
|
static uint64_t leading_uint(const std::string& s) {
|
|
size_t i = 0;
|
|
while (i < s.size() && std::isdigit((unsigned char)s[i])) ++i;
|
|
if (i == 0) return 0;
|
|
return std::stoull(s.substr(0, i));
|
|
}
|
|
|
|
#ifndef __APPLE__
|
|
// file-exists helper (Linux dhcp detection)
|
|
static bool file_exists(const std::string& path) {
|
|
struct stat st;
|
|
return stat(path.c_str(), &st) == 0;
|
|
}
|
|
#endif // !__APPLE__
|
|
|
|
// ── Minimal JSON helpers for lsblk / ip -j / system_profiler output ──────────
|
|
#ifndef __APPLE__
|
|
// Extract the value of a JSON string or scalar field by key.
|
|
// Returns "" for null/missing.
|
|
static std::string json_str(const std::string& json, const std::string& key) {
|
|
// Accept both "key":"v" and "key": "v"
|
|
for (auto sep : {std::string("\"") + key + "\":\"",
|
|
std::string("\"") + key + "\": \""}) {
|
|
size_t pos = json.find(sep);
|
|
if (pos == std::string::npos) continue;
|
|
pos += sep.size();
|
|
std::string result;
|
|
while (pos < json.size() && json[pos] != '"') {
|
|
if (json[pos] == '\\') {
|
|
++pos;
|
|
if (pos < json.size()) {
|
|
char c = json[pos];
|
|
if (c == 'n') result += '\n';
|
|
else if (c == 't') result += '\t';
|
|
else if (c == '\\') result += '\\';
|
|
else if (c == '"') result += '"';
|
|
else result += c;
|
|
}
|
|
} else {
|
|
result += json[pos];
|
|
}
|
|
++pos;
|
|
}
|
|
return result;
|
|
}
|
|
// Try null / numeric / bool (no quotes)
|
|
for (auto sep : {std::string("\"") + key + "\":",
|
|
std::string("\"") + key + "\": "}) {
|
|
size_t pos = json.find(sep);
|
|
if (pos == std::string::npos) continue;
|
|
pos += sep.size();
|
|
while (pos < json.size() && std::isspace((unsigned char)json[pos])) ++pos;
|
|
if (pos >= json.size() || json[pos] == '"') continue; // quoted handled above
|
|
if (json[pos] == 'n') return ""; // null
|
|
std::string result;
|
|
while (pos < json.size() && json[pos] != ',' && json[pos] != '}' &&
|
|
json[pos] != ']' && !std::isspace((unsigned char)json[pos]))
|
|
result += json[pos++];
|
|
return result;
|
|
}
|
|
return "";
|
|
}
|
|
|
|
// Split a JSON array of objects into individual object strings (outer array only).
|
|
static std::vector<std::string> json_array_objects(const std::string& json,
|
|
const std::string& array_key) {
|
|
std::string search = "\"" + array_key + "\":";
|
|
size_t pos = json.find(search);
|
|
if (pos == std::string::npos) {
|
|
search = "\"" + array_key + "\": ";
|
|
pos = json.find(search);
|
|
}
|
|
if (pos == std::string::npos) return {};
|
|
pos = json.find('[', pos);
|
|
if (pos == std::string::npos) return {};
|
|
++pos; // skip '['
|
|
|
|
std::vector<std::string> result;
|
|
int depth = 0;
|
|
size_t obj_start = std::string::npos;
|
|
bool in_string = false;
|
|
char prev = '\0';
|
|
|
|
for (size_t i = pos; i < json.size(); ++i) {
|
|
char c = json[i];
|
|
// Track string boundaries to avoid treating braces inside strings
|
|
if (c == '"' && prev != '\\') { in_string = !in_string; }
|
|
if (!in_string) {
|
|
if (c == '{') {
|
|
if (depth == 0) obj_start = i;
|
|
++depth;
|
|
} else if (c == '}') {
|
|
--depth;
|
|
if (depth == 0 && obj_start != std::string::npos) {
|
|
result.push_back(json.substr(obj_start, i - obj_start + 1));
|
|
obj_start = std::string::npos;
|
|
}
|
|
} else if (c == ']' && depth == 0) {
|
|
break;
|
|
}
|
|
}
|
|
prev = c;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// Extract all string values from a JSON array field (flat, one level).
|
|
static std::vector<std::string> json_array_strings(const std::string& json,
|
|
const std::string& key) {
|
|
std::string search = "\"" + key + "\":[";
|
|
size_t pos = json.find(search);
|
|
if (pos == std::string::npos) {
|
|
search = "\"" + key + "\": [";
|
|
pos = json.find(search);
|
|
}
|
|
if (pos == std::string::npos) return {};
|
|
pos = json.find('[', pos) + 1;
|
|
std::vector<std::string> result;
|
|
while (pos < json.size()) {
|
|
while (pos < json.size() && std::isspace((unsigned char)json[pos])) ++pos;
|
|
if (pos >= json.size() || json[pos] == ']') break;
|
|
if (json[pos] == '"') {
|
|
++pos;
|
|
std::string v;
|
|
while (pos < json.size() && json[pos] != '"') v += json[pos++];
|
|
if (pos < json.size()) ++pos; // skip closing '"'
|
|
result.push_back(v);
|
|
} else {
|
|
++pos; // skip other chars
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
#endif // !__APPLE__ (json_array_objects)
|
|
|
|
// Trim leading/trailing whitespace from a string.
|
|
static std::string trim(const std::string& s) {
|
|
size_t a = 0, b = s.size();
|
|
while (a < b && std::isspace((unsigned char)s[a])) ++a;
|
|
while (b > a && std::isspace((unsigned char)s[b-1])) --b;
|
|
return s.substr(a, b - a);
|
|
}
|
|
|
|
} // anonymous namespace
|
|
|
|
// ═════════════════════════════════════════════════════════════════════════════
|
|
// Public interface
|
|
// ═════════════════════════════════════════════════════════════════════════════
|
|
|
|
std::vector<DiscoveredPart> Discovery::discover_all() {
|
|
std::vector<DiscoveredPart> all;
|
|
for (auto& v : {discover_memory_sticks(), discover_memory_slots(),
|
|
discover_cpus(), discover_cpu_slots(),
|
|
discover_disks(), discover_nics()}) {
|
|
all.insert(all.end(), v.begin(), v.end());
|
|
}
|
|
return all;
|
|
}
|
|
|
|
std::vector<DiscoveredPart> Discovery::discover(const std::string& type_name) {
|
|
if (type_name == PTYPE_MEMORY_STICK) return discover_memory_sticks();
|
|
if (type_name == PTYPE_MEMORY_SLOT) return discover_memory_slots();
|
|
if (type_name == PTYPE_CPU) return discover_cpus();
|
|
if (type_name == PTYPE_CPU_SLOT) return discover_cpu_slots();
|
|
if (type_name == PTYPE_DISK) return discover_disks();
|
|
if (type_name == PTYPE_NIC) return discover_nics();
|
|
std::cerr << "discover: unknown type '" << type_name << "'\n";
|
|
return {};
|
|
}
|
|
|
|
// ═════════════════════════════════════════════════════════════════════════════
|
|
// Memory sticks
|
|
// ═════════════════════════════════════════════════════════════════════════════
|
|
|
|
std::vector<DiscoveredPart> Discovery::discover_memory_sticks() {
|
|
std::vector<DiscoveredPart> result;
|
|
|
|
#ifdef __APPLE__
|
|
// ── macOS: system_profiler SPMemoryDataType ───────────────────────────────
|
|
std::string out = run_cmd("system_profiler SPMemoryDataType 2>/dev/null");
|
|
if (out.empty()) {
|
|
std::cerr << "warning: system_profiler SPMemoryDataType returned no data\n";
|
|
return result;
|
|
}
|
|
|
|
// Each "BANK X/..." section is a slot; skip lines until we see "Size:"
|
|
// Parse simple indented key: value text
|
|
std::map<std::string, std::string> block;
|
|
std::string bank_name;
|
|
|
|
auto flush_block = [&]() {
|
|
if (block.empty()) return;
|
|
auto sit = block.find("Size");
|
|
// Skip empty/missing slots
|
|
if (sit == block.end() || sit->second == "Empty" || sit->second.empty()) {
|
|
block.clear(); return;
|
|
}
|
|
DiscoveredPart p;
|
|
p.type_name = PTYPE_MEMORY_STICK;
|
|
p.kv[K_SIZE_MB] = std::to_string(dmi_size_to_mb(sit->second));
|
|
if (block.count("Speed")) p.kv[K_SPEED_MHZ] = std::to_string(leading_uint(block["Speed"]));
|
|
if (block.count("Manufacturer")) p.kv[K_MANUFACTURER] = block["Manufacturer"];
|
|
if (block.count("Serial Number") && block["Serial Number"] != "-")
|
|
p.kv[K_SERIAL] = block["Serial Number"];
|
|
if (block.count("Type"))
|
|
p.kv[K_OTHER_INFO] = block["Type"];
|
|
// channel_config heuristic from bank name
|
|
if (!bank_name.empty()) {
|
|
std::string lname = bank_name;
|
|
std::transform(lname.begin(), lname.end(), lname.begin(), ::tolower);
|
|
if (lname.find("channel") != std::string::npos)
|
|
p.kv[K_CHANNEL_CONFIG] = "dual";
|
|
}
|
|
result.push_back(std::move(p));
|
|
block.clear();
|
|
};
|
|
|
|
std::istringstream ss(out);
|
|
std::string line;
|
|
while (std::getline(ss, line)) {
|
|
if (!line.empty() && line.back() == '\r') line.pop_back();
|
|
std::string t = trim(line);
|
|
if (t.empty()) continue;
|
|
// Bank header: e.g. " BANK 0/ChannelA-DIMM0:"
|
|
if (t.back() == ':' && t.find('/') != std::string::npos) {
|
|
flush_block();
|
|
bank_name = t.substr(0, t.size() - 1);
|
|
continue;
|
|
}
|
|
size_t colon = t.find(": ");
|
|
if (colon != std::string::npos)
|
|
block[t.substr(0, colon)] = t.substr(colon + 2);
|
|
}
|
|
flush_block();
|
|
|
|
#else
|
|
// ── Linux: dmidecode -t 17 ────────────────────────────────────────────────
|
|
auto blocks = parse_dmi("17");
|
|
if (blocks.empty()) {
|
|
std::cerr << "warning: dmidecode returned no Memory Device data "
|
|
"(may need root)\n";
|
|
return result;
|
|
}
|
|
|
|
for (auto& b : blocks) {
|
|
// Skip empty slots
|
|
if (!b.count("Size")) continue;
|
|
if (b["Size"] == "No Module Installed" || b["Size"].empty() ||
|
|
dmi_size_to_mb(b["Size"]) == 0) continue;
|
|
|
|
DiscoveredPart p;
|
|
p.type_name = PTYPE_MEMORY_STICK;
|
|
|
|
if (b.count("Serial Number") && b["Serial Number"] != "Not Specified" &&
|
|
b["Serial Number"] != "Unknown")
|
|
p.kv[K_SERIAL] = b["Serial Number"];
|
|
|
|
p.kv[K_SIZE_MB] = std::to_string(dmi_size_to_mb(b["Size"]));
|
|
|
|
if (b.count("Speed"))
|
|
p.kv[K_SPEED_MHZ] = std::to_string(leading_uint(b["Speed"]));
|
|
|
|
if (b.count("Manufacturer") && b["Manufacturer"] != "Not Specified")
|
|
p.kv[K_MANUFACTURER] = b["Manufacturer"];
|
|
|
|
// Channel config heuristic from Bank Locator
|
|
if (b.count("Bank Locator")) {
|
|
std::string bl = b["Bank Locator"];
|
|
std::transform(bl.begin(), bl.end(), bl.begin(), ::tolower);
|
|
if (bl.find("channel") != std::string::npos)
|
|
p.kv[K_CHANNEL_CONFIG] = "dual";
|
|
}
|
|
|
|
// other_info: Type + Form Factor
|
|
std::string other;
|
|
if (b.count("Type") && b["Type"] != "Unknown") other += b["Type"];
|
|
if (b.count("Form Factor") && b["Form Factor"] != "Unknown") {
|
|
if (!other.empty()) other += " ";
|
|
other += b["Form Factor"];
|
|
}
|
|
if (!other.empty()) p.kv[K_OTHER_INFO] = other;
|
|
|
|
if (b.count("Locator") && !b["Locator"].empty())
|
|
p.kv[K_LOCATOR] = b["Locator"];
|
|
|
|
result.push_back(std::move(p));
|
|
}
|
|
#endif
|
|
|
|
return result;
|
|
}
|
|
|
|
// ═════════════════════════════════════════════════════════════════════════════
|
|
// Memory slots
|
|
// ═════════════════════════════════════════════════════════════════════════════
|
|
|
|
std::vector<DiscoveredPart> Discovery::discover_memory_slots() {
|
|
std::vector<DiscoveredPart> result;
|
|
|
|
#ifdef __APPLE__
|
|
// On macOS, use system_profiler to infer slot count from bank info
|
|
std::string out = run_cmd("system_profiler SPMemoryDataType 2>/dev/null");
|
|
if (out.empty()) return result;
|
|
|
|
uint64_t allowed_speed = 0;
|
|
int slot_count = 0;
|
|
|
|
std::istringstream ss(out);
|
|
std::string line;
|
|
while (std::getline(ss, line)) {
|
|
std::string t = trim(line);
|
|
if (t.back() == ':' && t.find('/') != std::string::npos) {
|
|
++slot_count;
|
|
}
|
|
if (t.rfind("Speed:", 0) == 0 && allowed_speed == 0) {
|
|
allowed_speed = leading_uint(t.substr(7));
|
|
}
|
|
}
|
|
|
|
for (int s = 0; s < slot_count; ++s) {
|
|
DiscoveredPart p;
|
|
p.type_name = PTYPE_MEMORY_SLOT;
|
|
if (allowed_speed) p.kv[K_ALLOWED_SPEED] = std::to_string(allowed_speed);
|
|
p.kv[K_INSTALLED_STICK] = "NULL";
|
|
result.push_back(std::move(p));
|
|
}
|
|
#else
|
|
// ── Linux: dmidecode -t 17 (all slots) + -t 16 for max capacity ───────────
|
|
auto blocks17 = parse_dmi("17");
|
|
if (blocks17.empty()) {
|
|
std::cerr << "warning: dmidecode returned no Memory Device data "
|
|
"(may need root)\n";
|
|
return result;
|
|
}
|
|
|
|
// Get max allowed size from dmidecode -t 16 (Physical Memory Array)
|
|
uint64_t max_size_mb = 0;
|
|
{
|
|
auto blocks16 = parse_dmi("16");
|
|
for (auto& b : blocks16) {
|
|
if (b.count("Maximum Capacity")) {
|
|
uint64_t mc = dmi_size_to_mb(b["Maximum Capacity"]);
|
|
if (mc > max_size_mb) max_size_mb = mc;
|
|
}
|
|
}
|
|
}
|
|
|
|
for (auto& b : blocks17) {
|
|
DiscoveredPart p;
|
|
p.type_name = PTYPE_MEMORY_SLOT;
|
|
|
|
// Allowed speed
|
|
uint64_t spd = 0;
|
|
if (b.count("Configured Memory Speed"))
|
|
spd = leading_uint(b["Configured Memory Speed"]);
|
|
if (!spd && b.count("Speed"))
|
|
spd = leading_uint(b["Speed"]);
|
|
if (spd) p.kv[K_ALLOWED_SPEED] = std::to_string(spd);
|
|
|
|
// Allowed size (per-slot approximation: total max / slot count)
|
|
if (max_size_mb > 0 && !blocks17.empty())
|
|
p.kv[K_ALLOWED_SIZE] = std::to_string(
|
|
max_size_mb / static_cast<uint64_t>(blocks17.size()));
|
|
|
|
p.kv[K_INSTALLED_STICK] = "NULL";
|
|
if (b.count("Locator") && !b["Locator"].empty())
|
|
p.kv[K_LOCATOR] = b["Locator"];
|
|
result.push_back(std::move(p));
|
|
}
|
|
#endif
|
|
|
|
return result;
|
|
}
|
|
|
|
// ═════════════════════════════════════════════════════════════════════════════
|
|
// CPUs
|
|
// ═════════════════════════════════════════════════════════════════════════════
|
|
|
|
std::vector<DiscoveredPart> Discovery::discover_cpus() {
|
|
std::vector<DiscoveredPart> result;
|
|
|
|
#ifdef __APPLE__
|
|
std::string brand = run_cmd("sysctl -n machdep.cpu.brand_string 2>/dev/null");
|
|
std::string phys = run_cmd("sysctl -n hw.physicalcpu 2>/dev/null");
|
|
std::string logi = run_cmd("sysctl -n hw.logicalcpu 2>/dev/null");
|
|
std::string freq = run_cmd("sysctl -n hw.cpufrequency 2>/dev/null");
|
|
|
|
if (brand.empty()) {
|
|
std::cerr << "warning: sysctl cpu info unavailable\n";
|
|
return result;
|
|
}
|
|
|
|
DiscoveredPart p;
|
|
p.type_name = PTYPE_CPU;
|
|
p.kv[K_NAME] = brand;
|
|
|
|
// Try to extract speed from brand string e.g. "@ 2.60GHz"
|
|
double speed_ghz = 0.0;
|
|
auto at_pos = brand.find(" @ ");
|
|
if (at_pos != std::string::npos) {
|
|
std::string sp = brand.substr(at_pos + 3);
|
|
try { speed_ghz = std::stod(sp); } catch (...) {}
|
|
}
|
|
if (speed_ghz == 0.0 && !freq.empty()) {
|
|
try { speed_ghz = std::stoull(freq) / 1e9; } catch (...) {}
|
|
}
|
|
if (speed_ghz > 0.0) {
|
|
char buf[32];
|
|
snprintf(buf, sizeof(buf), "%.2f", speed_ghz);
|
|
p.kv[K_SPEED_GHZ] = buf;
|
|
}
|
|
|
|
if (!phys.empty()) p.kv[K_CORES] = phys;
|
|
if (!logi.empty()) p.kv[K_THREADS] = logi;
|
|
|
|
// Manufacturer heuristic
|
|
std::string lower_brand = brand;
|
|
std::transform(lower_brand.begin(), lower_brand.end(), lower_brand.begin(), ::tolower);
|
|
if (lower_brand.find("intel") != std::string::npos) p.kv[K_MANUFACTURER] = "Intel";
|
|
else if (lower_brand.find("amd") != std::string::npos) p.kv[K_MANUFACTURER] = "AMD";
|
|
else if (lower_brand.find("apple") != std::string::npos) p.kv[K_MANUFACTURER] = "Apple";
|
|
|
|
result.push_back(std::move(p));
|
|
|
|
#else
|
|
// ── Linux: dmidecode -t 4 ─────────────────────────────────────────────────
|
|
auto blocks = parse_dmi("4");
|
|
if (blocks.empty()) {
|
|
std::cerr << "warning: dmidecode returned no Processor data "
|
|
"(may need root)\n";
|
|
return result;
|
|
}
|
|
|
|
for (auto& b : blocks) {
|
|
// Skip phantom entries (iLO processor, "Other" type, absent Status)
|
|
if (!b.count("Status") || b["Status"].find("Populated") == std::string::npos) continue;
|
|
if (b.count("Type") && b["Type"].find("Central Processor") == std::string::npos) continue;
|
|
|
|
DiscoveredPart p;
|
|
p.type_name = PTYPE_CPU;
|
|
|
|
if (b.count("ID") && b["ID"] != "Not Specified")
|
|
p.kv[K_SERIAL] = b["ID"];
|
|
if (b.count("Version") && b["Version"] != "Not Specified")
|
|
p.kv[K_NAME] = b["Version"];
|
|
if (b.count("Manufacturer") && b["Manufacturer"] != "Not Specified")
|
|
p.kv[K_MANUFACTURER] = b["Manufacturer"];
|
|
|
|
if (b.count("Current Speed")) {
|
|
double mhz = static_cast<double>(leading_uint(b["Current Speed"]));
|
|
if (mhz > 0) {
|
|
char buf[32];
|
|
snprintf(buf, sizeof(buf), "%.3f", mhz / 1000.0);
|
|
p.kv[K_SPEED_GHZ] = buf;
|
|
}
|
|
}
|
|
if (b.count("Core Count")) p.kv[K_CORES] = b["Core Count"];
|
|
if (b.count("Thread Count")) p.kv[K_THREADS] = b["Thread Count"];
|
|
|
|
if (b.count("Socket Designation") && b["Socket Designation"] != "Not Specified")
|
|
p.kv[K_SOCKET] = b["Socket Designation"];
|
|
|
|
result.push_back(std::move(p));
|
|
}
|
|
#endif
|
|
|
|
return result;
|
|
}
|
|
|
|
// ═════════════════════════════════════════════════════════════════════════════
|
|
// CPU slots
|
|
// ═════════════════════════════════════════════════════════════════════════════
|
|
|
|
std::vector<DiscoveredPart> Discovery::discover_cpu_slots() {
|
|
std::vector<DiscoveredPart> result;
|
|
|
|
#ifdef __APPLE__
|
|
// On macOS we can only infer 1 slot from sysctl
|
|
if (run_cmd("sysctl -n machdep.cpu.brand_string 2>/dev/null").empty())
|
|
return result;
|
|
|
|
DiscoveredPart p;
|
|
p.type_name = PTYPE_CPU_SLOT;
|
|
p.kv[K_FORM_FACTOR] = "unknown";
|
|
p.kv[K_INSTALLED_CPU] = "NULL";
|
|
result.push_back(std::move(p));
|
|
#else
|
|
auto blocks = parse_dmi("4");
|
|
if (blocks.empty()) {
|
|
std::cerr << "warning: dmidecode returned no Processor data "
|
|
"(may need root)\n";
|
|
return result;
|
|
}
|
|
|
|
for (auto& b : blocks) {
|
|
// Only real CPU sockets: must have a recognizable Status (Populated or Unpopulated)
|
|
// and must be of type Central Processor
|
|
if (!b.count("Status")) continue;
|
|
const std::string& status = b["Status"];
|
|
bool is_populated = status.find("Populated") != std::string::npos;
|
|
bool is_unpopulated = status.find("Unpopulated") != std::string::npos;
|
|
if (!is_populated && !is_unpopulated) continue; // Skip "Other", empty, or phantom entries
|
|
if (b.count("Type") && b["Type"].find("Central Processor") == std::string::npos) continue;
|
|
|
|
DiscoveredPart p;
|
|
p.type_name = PTYPE_CPU_SLOT;
|
|
if (b.count("Upgrade")) p.kv[K_FORM_FACTOR] = b["Upgrade"];
|
|
else p.kv[K_FORM_FACTOR] = "unknown";
|
|
p.kv[K_INSTALLED_CPU] = "NULL";
|
|
if (b.count("Socket Designation") && b["Socket Designation"] != "Not Specified")
|
|
p.kv[K_SOCKET] = b["Socket Designation"];
|
|
result.push_back(std::move(p));
|
|
}
|
|
#endif
|
|
|
|
return result;
|
|
}
|
|
|
|
// ═════════════════════════════════════════════════════════════════════════════
|
|
// Disks
|
|
// ═════════════════════════════════════════════════════════════════════════════
|
|
|
|
std::vector<DiscoveredPart> Discovery::discover_disks() {
|
|
std::vector<DiscoveredPart> result;
|
|
|
|
#ifdef __APPLE__
|
|
// ── macOS: diskutil info for each disk ────────────────────────────────────
|
|
// Enumerate whole disks via XML plist from diskutil list -plist
|
|
std::vector<std::string> all_disks;
|
|
{
|
|
std::string plist_out = run_cmd("diskutil list -plist 2>/dev/null");
|
|
if (!plist_out.empty()) {
|
|
// Parse <key>WholeDisks</key><array><string>disk0</string>...</array>
|
|
size_t key_pos = plist_out.find("<key>WholeDisks</key>");
|
|
if (key_pos != std::string::npos) {
|
|
size_t arr_start = plist_out.find("<array>", key_pos);
|
|
size_t arr_end = plist_out.find("</array>", key_pos);
|
|
if (arr_start != std::string::npos && arr_end != std::string::npos) {
|
|
std::string arr_content = plist_out.substr(arr_start, arr_end - arr_start);
|
|
size_t p = 0;
|
|
while ((p = arr_content.find("<string>", p)) != std::string::npos) {
|
|
p += 8;
|
|
size_t e = arr_content.find("</string>", p);
|
|
if (e == std::string::npos) break;
|
|
all_disks.push_back(arr_content.substr(p, e - p));
|
|
p = e + 9;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (all_disks.empty()) {
|
|
// fall back: text diskutil list output
|
|
std::string txt = run_cmd("diskutil list 2>/dev/null");
|
|
std::istringstream ss(txt);
|
|
std::string line;
|
|
while (std::getline(ss, line)) {
|
|
std::string t = trim(line);
|
|
if (t.rfind("/dev/disk", 0) == 0) {
|
|
size_t sp = t.find(' ');
|
|
std::string dev = (sp != std::string::npos) ? t.substr(5, sp - 5) : t.substr(5);
|
|
if (dev.find('s') == std::string::npos) all_disks.push_back(dev);
|
|
}
|
|
}
|
|
}
|
|
if (all_disks.empty()) {
|
|
std::cerr << "warning: could not enumerate disks via diskutil\n";
|
|
return result;
|
|
}
|
|
}
|
|
|
|
for (auto& disk_name : all_disks) {
|
|
std::string info = run_cmd("diskutil info -plist /dev/" + disk_name + " 2>/dev/null");
|
|
if (info.empty()) continue;
|
|
|
|
// Parse plist-style XML (very simplified)
|
|
// Extract key/value pairs from <key>K</key><string>V</string>
|
|
std::map<std::string, std::string> plist;
|
|
std::istringstream ss(info);
|
|
std::string line;
|
|
std::string cur_key;
|
|
while (std::getline(ss, line)) {
|
|
std::string t = trim(line);
|
|
auto tag_val = [&](const std::string& tag) -> std::string {
|
|
std::string open = "<" + tag + ">";
|
|
std::string close = "</" + tag + ">";
|
|
size_t p0 = t.find(open);
|
|
if (p0 == std::string::npos) return "";
|
|
p0 += open.size();
|
|
size_t p1 = t.find(close, p0);
|
|
if (p1 == std::string::npos) return "";
|
|
return t.substr(p0, p1 - p0);
|
|
};
|
|
std::string k = tag_val("key");
|
|
if (!k.empty()) { cur_key = k; continue; }
|
|
if (!cur_key.empty()) {
|
|
for (auto& vtag : {"string", "integer", "real"}) {
|
|
std::string v = tag_val(vtag);
|
|
if (!v.empty()) { plist[cur_key] = v; cur_key = ""; break; }
|
|
}
|
|
if (t == "<true/>") { plist[cur_key] = "true"; cur_key = ""; }
|
|
if (t == "<false/>") { plist[cur_key] = "false"; cur_key = ""; }
|
|
}
|
|
}
|
|
|
|
// Skip virtual disks
|
|
auto ct = plist.count("VirtualOrPhysical") ? plist["VirtualOrPhysical"] : "";
|
|
if (ct == "Virtual") continue;
|
|
|
|
DiscoveredPart p;
|
|
p.type_name = PTYPE_DISK;
|
|
|
|
if (plist.count("MediaName")) p.kv[K_MODEL] = plist["MediaName"];
|
|
if (plist.count("IORegistryEntryName") && !p.kv.count(K_MODEL))
|
|
p.kv[K_MODEL] = plist["IORegistryEntryName"];
|
|
|
|
// Size
|
|
if (plist.count("TotalSize"))
|
|
p.kv[K_DISK_SIZE] = std::to_string(
|
|
static_cast<uint64_t>(std::stoull(plist["TotalSize"]) / 1000000000ULL));
|
|
|
|
// Type
|
|
bool solid = false;
|
|
if (plist.count("SolidState")) solid = (plist["SolidState"] == "true");
|
|
std::string protocol = plist.count("BusProtocol") ? plist["BusProtocol"] : "";
|
|
std::string conn = protocol;
|
|
std::string dtype;
|
|
std::string lower_proto = protocol;
|
|
std::transform(lower_proto.begin(), lower_proto.end(), lower_proto.begin(), ::tolower);
|
|
if (lower_proto.find("nvme") != std::string::npos ||
|
|
lower_proto.find("pcie") != std::string::npos) {
|
|
dtype = "nvme"; conn = "NVMe";
|
|
} else if (solid) {
|
|
dtype = "ssd";
|
|
} else {
|
|
dtype = "hdd";
|
|
}
|
|
if (!dtype.empty()) p.kv[K_DISK_TYPE] = dtype;
|
|
if (!conn.empty()) p.kv[K_CONN_TYPE] = conn;
|
|
|
|
// Speed heuristic
|
|
uint64_t conn_speed = 0;
|
|
if (lower_proto.find("nvme") != std::string::npos) conn_speed = 3500;
|
|
else if (lower_proto.find("sata") != std::string::npos) conn_speed = 600;
|
|
else if (lower_proto.find("usb") != std::string::npos) conn_speed = 480;
|
|
if (conn_speed) p.kv[K_CONN_SPEED] = std::to_string(conn_speed);
|
|
p.kv[K_DISK_SPEED] = "0";
|
|
p.kv[K_AGE_YEARS] = "-1";
|
|
|
|
// Partitions — use diskutil list <disk> text output for partition names
|
|
std::string parts_txt = run_cmd("diskutil list /dev/" + disk_name + " 2>/dev/null");
|
|
std::vector<std::string> part_ids, part_sizes;
|
|
{
|
|
std::istringstream ps(parts_txt);
|
|
std::string pline;
|
|
while (std::getline(ps, pline)) {
|
|
std::string pt = trim(pline);
|
|
// lines containing "disk0s1" etc.
|
|
if (pt.find(disk_name + "s") != std::string::npos) {
|
|
// Extract partition dev name (last token-ish)
|
|
std::istringstream ts(pt);
|
|
std::string tok, last_tok;
|
|
while (ts >> tok) last_tok = tok;
|
|
if (!last_tok.empty() && last_tok.find(disk_name) != std::string::npos)
|
|
part_ids.push_back(last_tok);
|
|
}
|
|
}
|
|
}
|
|
if (!part_ids.empty()) {
|
|
std::string pid_str, psz_str;
|
|
for (size_t ki = 0; ki < part_ids.size(); ++ki) {
|
|
if (ki) { pid_str += LS; psz_str += LS; }
|
|
pid_str += part_ids[ki];
|
|
psz_str += "0";
|
|
}
|
|
p.kv[K_PARTITION_IDS] = pid_str;
|
|
p.kv[K_PARTITION_SIZES] = psz_str;
|
|
}
|
|
|
|
p.kv[K_VM_HOSTNAMES] = "";
|
|
p.kv[K_VM_SERVER_IDS] = "";
|
|
|
|
result.push_back(std::move(p));
|
|
}
|
|
|
|
#else
|
|
// ── Linux: lsblk -J ──────────────────────────────────────────────────────
|
|
std::string out = run_cmd(
|
|
"lsblk -J -o NAME,SIZE,TYPE,ROTA,SERIAL,MODEL,VENDOR,TRAN,"
|
|
"LOG-SEC,PHY-SEC,MOUNTPOINTS 2>/dev/null");
|
|
if (out.empty()) {
|
|
std::cerr << "warning: lsblk returned no data\n";
|
|
return result;
|
|
}
|
|
|
|
auto devices = json_array_objects(out, "blockdevices");
|
|
for (auto& dev : devices) {
|
|
// Only top-level disks
|
|
std::string type = json_str(dev, "type");
|
|
if (type != "disk") continue;
|
|
|
|
std::string dev_name = json_str(dev, "name");
|
|
|
|
DiscoveredPart p;
|
|
p.type_name = PTYPE_DISK;
|
|
|
|
std::string serial = json_str(dev, "serial");
|
|
std::string model = trim(json_str(dev, "model"));
|
|
std::string vendor = trim(json_str(dev, "vendor"));
|
|
std::string rota = json_str(dev, "rota");
|
|
std::string tran = json_str(dev, "tran");
|
|
std::string size_str = json_str(dev, "size");
|
|
|
|
if (!serial.empty() && serial != "null") p.kv[K_SERIAL] = serial;
|
|
if (!model.empty()) p.kv[K_MODEL] = model;
|
|
if (!vendor.empty()) p.kv[K_MANUFACTURER] = vendor;
|
|
|
|
// disk_type
|
|
std::string lower_tran = tran;
|
|
std::transform(lower_tran.begin(), lower_tran.end(), lower_tran.begin(), ::tolower);
|
|
std::string dtype;
|
|
if (lower_tran.find("nvme") != std::string::npos) dtype = "nvme";
|
|
else if (rota == "0") dtype = "ssd";
|
|
else dtype = "hdd";
|
|
p.kv[K_DISK_TYPE] = dtype;
|
|
p.kv[K_CONN_TYPE] = tran.empty() ? "unknown" : tran;
|
|
|
|
// connection speed heuristic
|
|
uint64_t conn_speed = 0;
|
|
if (lower_tran.find("nvme") != std::string::npos) conn_speed = 3500;
|
|
else if (lower_tran.find("sata") != std::string::npos) conn_speed = 600;
|
|
else if (lower_tran.find("usb") != std::string::npos) conn_speed = 480;
|
|
else if (lower_tran.find("sas") != std::string::npos) conn_speed = 1200;
|
|
p.kv[K_CONN_SPEED] = std::to_string(conn_speed);
|
|
p.kv[K_DISK_SPEED] = "0";
|
|
p.kv[K_DISK_SIZE] = std::to_string(size_to_gb(size_str));
|
|
p.kv[K_AGE_YEARS] = "-1";
|
|
|
|
// Partitions from "children" array
|
|
auto children = json_array_objects(dev, "children");
|
|
std::vector<std::string> part_ids, part_sizes;
|
|
for (auto& child : children) {
|
|
if (json_str(child, "type") == "part") {
|
|
part_ids.push_back(json_str(child, "name"));
|
|
part_sizes.push_back(std::to_string(size_to_gb(json_str(child, "size"))));
|
|
}
|
|
}
|
|
if (!part_ids.empty()) {
|
|
std::string pid_str, psz_str;
|
|
for (size_t ki = 0; ki < part_ids.size(); ++ki) {
|
|
if (ki) { pid_str += LS; psz_str += LS; }
|
|
pid_str += part_ids[ki];
|
|
psz_str += part_sizes[ki];
|
|
}
|
|
p.kv[K_PARTITION_IDS] = pid_str;
|
|
p.kv[K_PARTITION_SIZES] = psz_str;
|
|
}
|
|
|
|
p.kv[K_VM_HOSTNAMES] = "";
|
|
p.kv[K_VM_SERVER_IDS] = "";
|
|
|
|
// Handle serial collisions: if another disk in result already has the same serial,
|
|
// differentiate both by appending the device name to the serial.
|
|
if (!dev_name.empty()) {
|
|
p.kv[K_DEVICE_NAME] = dev_name;
|
|
auto ser_it = p.kv.find(K_SERIAL);
|
|
if (ser_it != p.kv.end()) {
|
|
for (auto& prev : result) {
|
|
auto prev_ser = prev.kv.find(K_SERIAL);
|
|
if (prev_ser != prev.kv.end() && prev_ser->second == ser_it->second) {
|
|
// Collision: suffix both with their device names
|
|
auto prev_dev = prev.kv.find(K_DEVICE_NAME);
|
|
std::string prev_name = (prev_dev != prev.kv.end()) ? prev_dev->second : "";
|
|
if (!prev_name.empty())
|
|
prev_ser->second += ":" + prev_name;
|
|
ser_it->second += ":" + dev_name;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
result.push_back(std::move(p));
|
|
}
|
|
#endif
|
|
|
|
return result;
|
|
}
|
|
// ═════════════════════════════════════════════════════════════════════════════
|
|
|
|
std::vector<DiscoveredPart> Discovery::discover_nics() {
|
|
std::vector<DiscoveredPart> result;
|
|
|
|
#ifdef __APPLE__
|
|
// ── macOS: networksetup + ifconfig ────────────────────────────────────────
|
|
// networksetup -listallhardwareports gives:
|
|
// Hardware Port: Ethernet
|
|
// Device: en0
|
|
// Ethernet Address: xx:xx:xx:xx:xx:xx
|
|
std::string ns_out = run_cmd("networksetup -listallhardwareports 2>/dev/null");
|
|
if (ns_out.empty()) {
|
|
std::cerr << "warning: networksetup not available\n";
|
|
return result;
|
|
}
|
|
|
|
struct NsPort { std::string port, device, mac; };
|
|
std::vector<NsPort> ports;
|
|
{
|
|
NsPort cur;
|
|
std::istringstream ss(ns_out);
|
|
std::string line;
|
|
while (std::getline(ss, line)) {
|
|
std::string t = trim(line);
|
|
auto extract = [&](const std::string& prefix) -> std::string {
|
|
if (t.rfind(prefix, 0) == 0) return trim(t.substr(prefix.size()));
|
|
return "";
|
|
};
|
|
auto pv = extract("Hardware Port: ");
|
|
if (!pv.empty()) { if (!cur.device.empty()) ports.push_back(cur); cur = {pv, "", ""}; continue; }
|
|
auto dv = extract("Device: ");
|
|
if (!dv.empty()) { cur.device = dv; continue; }
|
|
auto mv = extract("Ethernet Address: ");
|
|
if (!mv.empty()) { cur.mac = mv; continue; }
|
|
}
|
|
if (!cur.device.empty()) ports.push_back(cur);
|
|
}
|
|
|
|
for (auto& pt : ports) {
|
|
if (pt.device.empty()) continue;
|
|
// Skip loopback
|
|
if (pt.device == "lo0" || pt.device.rfind("lo", 0) == 0) continue;
|
|
|
|
DiscoveredPart p;
|
|
p.type_name = PTYPE_NIC;
|
|
p.kv[K_MAC] = pt.mac;
|
|
p.kv[K_MODEL] = pt.port;
|
|
|
|
// connection type
|
|
std::string lower_port = pt.port;
|
|
std::transform(lower_port.begin(), lower_port.end(), lower_port.begin(), ::tolower);
|
|
if (lower_port.find("wi-fi") != std::string::npos ||
|
|
lower_port.find("wifi") != std::string::npos ||
|
|
lower_port.find("wlan") != std::string::npos ||
|
|
lower_port.find("airport") != std::string::npos)
|
|
p.kv[K_CONN_TYPE] = "wifi";
|
|
else
|
|
p.kv[K_CONN_TYPE] = "ethernet";
|
|
|
|
// IPs via ifconfig
|
|
std::string ic_out = run_cmd("ifconfig " + pt.device + " 2>/dev/null");
|
|
std::vector<std::string> ips;
|
|
{
|
|
std::istringstream ss(ic_out);
|
|
std::string line;
|
|
while (std::getline(ss, line)) {
|
|
std::string t = trim(line);
|
|
if (t.rfind("inet ", 0) == 0 || t.rfind("inet6 ", 0) == 0) {
|
|
bool v6 = t[4] == '6';
|
|
std::string rest = t.substr(v6 ? 6 : 5);
|
|
// Extract addr and prefix/netmask
|
|
std::istringstream ts(rest);
|
|
std::string addr;
|
|
ts >> addr;
|
|
// Skip link-local for IPv6 unless it's the only address
|
|
if (v6 && addr.rfind("fe80", 0) == 0) continue;
|
|
std::string prefix_str;
|
|
std::string tok;
|
|
while (ts >> tok) {
|
|
if (tok == "prefixlen" || tok == "netmask") {
|
|
ts >> prefix_str;
|
|
break;
|
|
}
|
|
}
|
|
if (!prefix_str.empty() && prefix_str.rfind("0x", 0) == 0) {
|
|
// Convert hex netmask to prefix length
|
|
uint32_t mask = static_cast<uint32_t>(std::stoul(prefix_str, nullptr, 16));
|
|
int bits = 0;
|
|
while (mask) { bits += (mask & 1); mask >>= 1; }
|
|
prefix_str = std::to_string(bits);
|
|
}
|
|
std::string entry = addr;
|
|
if (!prefix_str.empty()) entry += "/" + prefix_str;
|
|
ips.push_back(entry);
|
|
}
|
|
}
|
|
}
|
|
if (!ips.empty()) {
|
|
std::string ips_str;
|
|
for (size_t ki = 0; ki < ips.size(); ++ki) {
|
|
if (ki) ips_str += LS;
|
|
ips_str += ips[ki];
|
|
}
|
|
p.kv[K_IPS] = ips_str;
|
|
}
|
|
|
|
p.kv[K_CONN_SPEED] = "0";
|
|
p.kv[K_AGE_YEARS] = "-1";
|
|
p.kv[K_DHCP] = "false";
|
|
|
|
result.push_back(std::move(p));
|
|
}
|
|
|
|
#else
|
|
// ── Linux: ip -j link show + ip -j addr show ──────────────────────────────
|
|
std::string link_out = run_cmd("ip -j link show 2>/dev/null");
|
|
if (link_out.empty()) {
|
|
std::cerr << "warning: ip -j link show failed\n";
|
|
return result;
|
|
}
|
|
|
|
auto ifaces = json_array_objects(link_out, ""); // top-level array
|
|
// ip -j link show returns a JSON array at the root level, not under a key
|
|
// Re-parse: split the root array
|
|
{
|
|
// json_array_objects looks for a named key; for root arrays we parse directly
|
|
ifaces.clear();
|
|
int depth = 0;
|
|
size_t obj_start = std::string::npos;
|
|
bool in_string = false;
|
|
char prev = '\0';
|
|
for (size_t i = 0; i < link_out.size(); ++i) {
|
|
char c = link_out[i];
|
|
if (c == '"' && prev != '\\') in_string = !in_string;
|
|
if (!in_string) {
|
|
if (c == '{') { if (depth == 0) obj_start = i; ++depth; }
|
|
else if (c == '}') {
|
|
--depth;
|
|
if (depth == 0 && obj_start != std::string::npos) {
|
|
ifaces.push_back(link_out.substr(obj_start, i - obj_start + 1));
|
|
obj_start = std::string::npos;
|
|
}
|
|
}
|
|
}
|
|
prev = c;
|
|
}
|
|
}
|
|
|
|
for (auto& iface : ifaces) {
|
|
std::string ifname = json_str(iface, "ifname");
|
|
std::string mac = json_str(iface, "address");
|
|
std::string link_type= json_str(iface, "link_type");
|
|
std::string flags_str= json_str(iface, "flags"); // may be array
|
|
|
|
if (ifname.empty()) continue;
|
|
// Only include physical NICs: those with a /device symlink pointing to a PCI entry.
|
|
// Virtual interfaces (bridges, veth, flannel, fwbr, vmbr, tap, etc.) have no device symlink.
|
|
{
|
|
std::string dev_path = "/sys/class/net/" + ifname + "/device";
|
|
struct stat st;
|
|
if (lstat(dev_path.c_str(), &st) != 0 || !S_ISLNK(st.st_mode)) continue;
|
|
}
|
|
|
|
// Enrich with PCI device description via lspci
|
|
std::string pci_model;
|
|
std::string pci_vendor;
|
|
{
|
|
std::string link = run_cmd("readlink /sys/class/net/" + ifname + "/device 2>/dev/null");
|
|
// link is like "../../../0000:01:00.0" — extract last path component
|
|
size_t last_slash = link.rfind('/');
|
|
if (last_slash != std::string::npos) link = link.substr(last_slash + 1);
|
|
// trim whitespace
|
|
while (!link.empty() && std::isspace((unsigned char)link.back())) link.pop_back();
|
|
if (!link.empty()) {
|
|
std::string lspci_out = run_cmd("lspci -s " + link + " 2>/dev/null");
|
|
// Format: "0000:01:00.0 Ethernet controller: Broadcom Corporation NetXtreme II..."
|
|
// Find first ": " after the address
|
|
size_t colon = lspci_out.find(": ");
|
|
if (colon != std::string::npos) {
|
|
std::string desc = lspci_out.substr(colon + 2);
|
|
while (!desc.empty() && std::isspace((unsigned char)desc.back())) desc.pop_back();
|
|
// Try to split "VendorName Description" — look for another ": " for sub-vendor
|
|
size_t sub = desc.find(": ");
|
|
if (sub != std::string::npos) {
|
|
pci_vendor = desc.substr(0, sub);
|
|
pci_model = desc.substr(sub + 2);
|
|
} else {
|
|
pci_model = desc;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
DiscoveredPart p;
|
|
p.type_name = PTYPE_NIC;
|
|
p.kv[K_MAC] = mac;
|
|
p.kv[K_MODEL] = pci_model.empty() ? link_type : pci_model;
|
|
if (!pci_vendor.empty()) p.kv[K_MANUFACTURER] = pci_vendor;
|
|
|
|
// connection type
|
|
std::string lower_ifname = ifname;
|
|
std::transform(lower_ifname.begin(), lower_ifname.end(),
|
|
lower_ifname.begin(), ::tolower);
|
|
std::string conn_type = "ethernet";
|
|
if (lower_ifname.find("wl") != std::string::npos ||
|
|
lower_ifname.find("wifi") != std::string::npos)
|
|
conn_type = "wifi";
|
|
p.kv[K_CONN_TYPE] = conn_type;
|
|
|
|
// IPs
|
|
std::string addr_out = run_cmd("ip -j addr show " + ifname + " 2>/dev/null");
|
|
std::vector<std::string> ips;
|
|
if (!addr_out.empty()) {
|
|
// find addr_info array inside the single interface object
|
|
std::string search = "\"addr_info\":";
|
|
size_t ai_pos = addr_out.find(search);
|
|
if (ai_pos != std::string::npos) {
|
|
std::string addr_slice = addr_out.substr(ai_pos);
|
|
auto addr_objs = json_array_objects(addr_slice, "addr_info");
|
|
for (auto& ao : addr_objs) {
|
|
std::string local = json_str(ao, "local");
|
|
std::string prefixlen = json_str(ao, "prefixlen");
|
|
if (local.empty()) continue;
|
|
// skip link-local IPv6
|
|
std::string ll = local;
|
|
std::transform(ll.begin(), ll.end(), ll.begin(), ::tolower);
|
|
if (ll.rfind("fe80", 0) == 0) continue;
|
|
std::string entry = local;
|
|
if (!prefixlen.empty()) entry += "/" + prefixlen;
|
|
ips.push_back(entry);
|
|
}
|
|
}
|
|
}
|
|
if (!ips.empty()) {
|
|
std::string ips_str;
|
|
for (size_t ki = 0; ki < ips.size(); ++ki) {
|
|
if (ki) ips_str += LS;
|
|
ips_str += ips[ki];
|
|
}
|
|
p.kv[K_IPS] = ips_str;
|
|
}
|
|
|
|
// DHCP detection
|
|
bool dhcp = file_exists("/run/dhclient." + ifname + ".pid") ||
|
|
file_exists("/run/dhcp-lease-" + ifname) ||
|
|
file_exists("/var/lib/dhcp/dhclient." + ifname + ".leases");
|
|
p.kv[K_DHCP] = dhcp ? "true" : "false";
|
|
|
|
// Link speed via sysfs
|
|
uint64_t speed = 0;
|
|
{
|
|
std::string speed_path = "/sys/class/net/" + ifname + "/speed";
|
|
FILE* sf = fopen(speed_path.c_str(), "r");
|
|
if (sf) {
|
|
int raw = 0;
|
|
if (fscanf(sf, "%d", &raw) == 1 && raw > 0) speed = static_cast<uint64_t>(raw);
|
|
fclose(sf);
|
|
}
|
|
}
|
|
p.kv[K_CONN_SPEED] = std::to_string(speed);
|
|
p.kv[K_AGE_YEARS] = "-1";
|
|
|
|
result.push_back(std::move(p));
|
|
}
|
|
#endif
|
|
|
|
return result;
|
|
}
|