feat: richer hardware discovery — CPU cache/voltage, memory type/bandwidth/part-no, GPU discovery

- CPU: max speed, bus MHz, L1/L2/L3 cache (from sysfs), voltage, socket type; /proc/cpuinfo fallback for non-root
- Memory sticks: DDR type, form factor, part number, rank, data width, theoretical bandwidth
- GPU: new part type discovered via lspci + /sys/class/drm + nvidia-smi; shows VRAM and display outputs
- discover-only tree updated to show all new fields

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Dan V 2026-04-01 01:20:33 +02:00
parent bf7a7937e0
commit 69113c1ea7
8 changed files with 654 additions and 13 deletions

View file

@ -291,7 +291,8 @@ std::vector<DiscoveredPart> Discovery::discover_all() {
std::vector<DiscoveredPart> all; std::vector<DiscoveredPart> all;
for (auto& v : {discover_memory_sticks(), discover_memory_slots(), for (auto& v : {discover_memory_sticks(), discover_memory_slots(),
discover_cpus(), discover_cpu_slots(), discover_cpus(), discover_cpu_slots(),
discover_disks(), discover_nics()}) { discover_disks(), discover_nics(),
discover_gpus()}) {
all.insert(all.end(), v.begin(), v.end()); all.insert(all.end(), v.begin(), v.end());
} }
return all; return all;
@ -304,6 +305,7 @@ std::vector<DiscoveredPart> Discovery::discover(const std::string& type_name) {
if (type_name == PTYPE_CPU_SLOT) return discover_cpu_slots(); if (type_name == PTYPE_CPU_SLOT) return discover_cpu_slots();
if (type_name == PTYPE_DISK) return discover_disks(); if (type_name == PTYPE_DISK) return discover_disks();
if (type_name == PTYPE_NIC) return discover_nics(); if (type_name == PTYPE_NIC) return discover_nics();
if (type_name == PTYPE_GPU) return discover_gpus();
std::cerr << "discover: unknown type '" << type_name << "'\n"; std::cerr << "discover: unknown type '" << type_name << "'\n";
return {}; return {};
} }
@ -423,6 +425,39 @@ std::vector<DiscoveredPart> Discovery::discover_memory_sticks() {
if (b.count("Locator") && !b["Locator"].empty()) if (b.count("Locator") && !b["Locator"].empty())
p.kv[K_LOCATOR] = b["Locator"]; p.kv[K_LOCATOR] = b["Locator"];
// Extended memory fields
if (b.count("Type") && b["Type"] != "Unknown" && b["Type"] != "Not Specified")
p.kv[K_MEM_TYPE] = b["Type"];
if (b.count("Form Factor") && b["Form Factor"] != "Unknown" && b["Form Factor"] != "Not Specified")
p.kv[K_FORM_FACTOR] = b["Form Factor"];
if (b.count("Part Number")) {
std::string pn = trim(b["Part Number"]);
if (!pn.empty() && pn != "Unknown" && pn != "Not Specified")
p.kv[K_PART_NUMBER] = pn;
}
if (b.count("Rank")) {
uint32_t rank = static_cast<uint32_t>(leading_uint(b["Rank"]));
if (rank > 0) p.kv[K_RANK] = std::to_string(rank);
}
uint32_t data_width = 0;
if (b.count("Data Width")) {
data_width = static_cast<uint32_t>(leading_uint(b["Data Width"]));
if (data_width > 0) p.kv[K_DATA_WIDTH] = std::to_string(data_width);
}
// Compute theoretical bandwidth: speed_mhz × 2 × (data_width / 8) MB/s
if (data_width > 0 && p.kv.count(K_SPEED_MHZ)) {
uint64_t spd = std::stoull(p.kv[K_SPEED_MHZ]);
if (spd > 0) {
uint64_t bw = spd * 2ULL * (data_width / 8);
p.kv[K_BANDWIDTH_MBPS] = std::to_string(bw);
}
}
result.push_back(std::move(p)); result.push_back(std::move(p));
} }
#endif #endif
@ -604,8 +639,193 @@ std::vector<DiscoveredPart> Discovery::discover_cpus() {
if (b.count("Socket Designation") && b["Socket Designation"] != "Not Specified") if (b.count("Socket Designation") && b["Socket Designation"] != "Not Specified")
p.kv[K_SOCKET] = b["Socket Designation"]; p.kv[K_SOCKET] = b["Socket Designation"];
// Extended CPU fields from dmidecode type 4
if (b.count("Max Speed")) {
double max_mhz = static_cast<double>(leading_uint(b["Max Speed"]));
if (max_mhz > 0) {
char buf[32];
snprintf(buf, sizeof(buf), "%.3f", max_mhz / 1000.0);
p.kv[K_MAX_SPEED_GHZ] = buf;
}
}
if (b.count("External Clock")) {
uint64_t bus_mhz = leading_uint(b["External Clock"]);
if (bus_mhz > 0) p.kv[K_BUS_MHZ] = std::to_string(bus_mhz);
}
if (b.count("Voltage")) {
std::string vs = b["Voltage"];
// trim " V" suffix
while (!vs.empty() && (vs.back() == 'V' || vs.back() == ' ')) vs.pop_back();
if (!vs.empty()) {
try {
double v = std::stod(vs);
char buf[32];
snprintf(buf, sizeof(buf), "%.1f", v);
p.kv[K_VOLTAGE_V] = buf;
} catch (...) {}
}
}
if (b.count("Upgrade") && b["Upgrade"] != "Unknown" && b["Upgrade"] != "Other") {
p.kv[K_SOCKET_TYPE] = b["Upgrade"];
}
result.push_back(std::move(p)); result.push_back(std::move(p));
} }
// Enrich CPUs with L1/L2/L3 cache sizes from sysfs
// Build a map: physical_package_id → first cpu index
{
std::map<int, int> pkg_to_first_cpu;
for (int ci = 0; ; ++ci) {
std::string pkg_path = "/sys/devices/system/cpu/cpu"
+ std::to_string(ci) + "/topology/physical_package_id";
FILE* pf = fopen(pkg_path.c_str(), "r");
if (!pf) break;
int pkg = -1;
fscanf(pf, "%d", &pkg);
fclose(pf);
if (pkg >= 0 && pkg_to_first_cpu.find(pkg) == pkg_to_first_cpu.end())
pkg_to_first_cpu[pkg] = ci;
}
// For each CPU entry in result (in order), pick socket 0, 1, 2 ...
int socket_idx = 0;
for (auto& p : result) {
if (p.type_name != PTYPE_CPU) { ++socket_idx; continue; }
auto it = pkg_to_first_cpu.find(socket_idx);
int cpu_idx = (it != pkg_to_first_cpu.end()) ? it->second : socket_idx;
// Walk cache indices for this cpu
uint32_t l1_kb = 0, l2_kb = 0, l3_kb = 0;
for (int idx = 0; idx < 8; ++idx) {
std::string base = "/sys/devices/system/cpu/cpu"
+ std::to_string(cpu_idx) + "/cache/index"
+ std::to_string(idx);
// Read level
int level = 0;
{
FILE* lf = fopen((base + "/level").c_str(), "r");
if (!lf) break;
fscanf(lf, "%d", &level);
fclose(lf);
}
// Read type
std::string cache_type;
{
FILE* tf = fopen((base + "/type").c_str(), "r");
if (tf) {
char tbuf[32] = {};
fscanf(tf, "%31s", tbuf);
fclose(tf);
cache_type = tbuf;
}
}
// Read size
std::string size_str;
{
FILE* sf = fopen((base + "/size").c_str(), "r");
if (sf) {
char sbuf[32] = {};
fscanf(sf, "%31s", sbuf);
fclose(sf);
size_str = sbuf;
}
}
if (size_str.empty()) continue;
// Parse size: "32K", "256K", "9216K", "6144K"
uint32_t kb = 0;
{
size_t ni = 0;
while (ni < size_str.size() && std::isdigit((unsigned char)size_str[ni])) ++ni;
if (ni > 0) {
kb = static_cast<uint32_t>(std::stoul(size_str.substr(0, ni)));
std::string unit = size_str.substr(ni);
if (!unit.empty() && (unit[0] == 'M' || unit[0] == 'm')) kb *= 1024;
}
}
if (kb == 0) continue;
if (level == 1 && cache_type == "Data") l1_kb += kb;
else if (level == 1 && cache_type == "Instruction") {} // skip icache
else if (level == 1 && cache_type == "Unified") l1_kb += kb;
else if (level == 2) l2_kb += kb;
else if (level == 3) l3_kb += kb;
}
if (l1_kb > 0) p.kv[K_CACHE_L1_KB] = std::to_string(l1_kb);
if (l2_kb > 0) p.kv[K_CACHE_L2_KB] = std::to_string(l2_kb);
if (l3_kb > 0) p.kv[K_CACHE_L3_KB] = std::to_string(l3_kb);
++socket_idx;
}
}
// /proc/cpuinfo fallback if dmidecode gave nothing
if (result.empty()) {
std::string cpuinfo = run_cmd("cat /proc/cpuinfo 2>/dev/null");
if (!cpuinfo.empty()) {
// Group by physical id
std::map<int, std::map<std::string,std::string>> by_phys;
std::map<std::string,std::string> cur_block;
int cur_phys = -1;
std::istringstream cs(cpuinfo);
std::string cl;
auto flush_cpuinfo = [&]() {
if (cur_phys < 0) return;
auto& target = by_phys[cur_phys];
for (auto& [k, v] : cur_block) {
if (target.find(k) == target.end())
target[k] = v;
}
};
while (std::getline(cs, cl)) {
if (cl.empty()) continue;
size_t colon = cl.find(':');
if (colon == std::string::npos) continue;
std::string key = trim(cl.substr(0, colon));
std::string val = trim(cl.substr(colon + 1));
if (key == "physical id") {
int phys = std::stoi(val);
if (phys != cur_phys) {
flush_cpuinfo();
cur_phys = phys;
cur_block.clear();
}
} else {
cur_block[key] = val;
}
}
flush_cpuinfo();
for (auto& [phys_id, fields] : by_phys) {
DiscoveredPart p;
p.type_name = PTYPE_CPU;
if (fields.count("model name")) p.kv[K_NAME] = fields["model name"];
if (fields.count("cpu MHz")) {
try {
double mhz = std::stod(fields["cpu MHz"]);
char buf[32];
snprintf(buf, sizeof(buf), "%.3f", mhz / 1000.0);
p.kv[K_SPEED_GHZ] = buf;
} catch (...) {}
}
if (fields.count("cpu cores")) p.kv[K_CORES] = fields["cpu cores"];
if (fields.count("siblings")) p.kv[K_THREADS] = fields["siblings"];
if (fields.count("cache size")) {
// "9216 KB"
uint32_t kb = static_cast<uint32_t>(leading_uint(fields["cache size"]));
if (kb > 0) p.kv[K_CACHE_L3_KB] = std::to_string(kb);
}
// Manufacturer from model name heuristic
if (fields.count("model name")) {
std::string lower = fields["model name"];
std::transform(lower.begin(), lower.end(), lower.begin(), ::tolower);
if (lower.find("intel") != std::string::npos) p.kv[K_MANUFACTURER] = "Intel";
else if (lower.find("amd") != std::string::npos) p.kv[K_MANUFACTURER] = "AMD";
}
// Use "Proc <N>" style socket designation based on physical id order
p.kv[K_SOCKET] = "Proc " + std::to_string(phys_id + 1);
result.push_back(std::move(p));
}
}
}
#endif #endif
return result; return result;
@ -1199,3 +1419,162 @@ std::vector<DiscoveredPart> Discovery::discover_nics() {
return result; return result;
} }
// ═════════════════════════════════════════════════════════════════════════════
// GPUs
// ═════════════════════════════════════════════════════════════════════════════
std::vector<DiscoveredPart> Discovery::discover_gpus() {
std::vector<DiscoveredPart> result;
#ifdef __APPLE__
// macOS GPU discovery not implemented
return result;
#else
std::string mm_out = run_cmd("lspci -mm 2>/dev/null");
if (mm_out.empty()) return result;
// Parse lspci -mm output. Each line:
// slot "class" "vendor" "device" ...
// (fields separated by spaces, quoted with double quotes)
auto parse_lspci_mm_line = [](const std::string& line)
-> std::vector<std::string> {
std::vector<std::string> fields;
size_t i = 0;
while (i < line.size()) {
while (i < line.size() && std::isspace((unsigned char)line[i])) ++i;
if (i >= line.size()) break;
if (line[i] == '"') {
++i;
std::string tok;
while (i < line.size() && line[i] != '"') tok += line[i++];
if (i < line.size()) ++i;
fields.push_back(tok);
} else {
std::string tok;
while (i < line.size() && !std::isspace((unsigned char)line[i])) tok += line[i++];
fields.push_back(tok);
}
}
return fields;
};
std::istringstream ss(mm_out);
std::string line;
while (std::getline(ss, line)) {
if (!line.empty() && line.back() == '\r') line.pop_back();
if (line.empty()) continue;
auto fields = parse_lspci_mm_line(line);
if (fields.size() < 4) continue;
std::string slot = fields[0];
std::string cls = fields[1];
std::string vendor = fields[2];
std::string device = fields[3];
// Filter for GPU classes
bool is_gpu = (cls.find("VGA compatible controller") != std::string::npos ||
cls.find("3D controller") != std::string::npos ||
cls.find("Display controller") != std::string::npos);
if (!is_gpu) continue;
DiscoveredPart p;
p.type_name = PTYPE_GPU;
p.kv[K_MANUFACTURER] = vendor;
p.kv[K_MODEL] = device;
p.kv[K_SERIAL] = slot; // PCI slot as stable ID
// NVIDIA VRAM via nvidia-smi
std::string lower_vendor = vendor;
std::transform(lower_vendor.begin(), lower_vendor.end(), lower_vendor.begin(), ::tolower);
if (lower_vendor.find("nvidia") != std::string::npos) {
std::string nsmi = run_cmd(
"nvidia-smi --query-gpu=index,name,memory.total --format=csv,noheader 2>/dev/null");
if (!nsmi.empty()) {
std::istringstream ns(nsmi);
std::string nline;
while (std::getline(ns, nline)) {
if (!nline.empty() && nline.back() == '\r') nline.pop_back();
size_t last_comma = nline.rfind(',');
if (last_comma == std::string::npos) continue;
std::string mem_str = trim(nline.substr(last_comma + 1));
uint64_t vram_mb = leading_uint(mem_str);
if (vram_mb > 0) {
p.kv[K_VRAM_MB] = std::to_string(vram_mb);
break;
}
}
}
}
// Find DRM card for this PCI slot
std::vector<std::string> outputs;
for (int card_idx = 0; card_idx < 8; ++card_idx) {
std::string card_name = "card" + std::to_string(card_idx);
std::string dev_link_path = "/sys/class/drm/" + card_name + "/device";
std::string resolved = run_cmd("readlink -f " + dev_link_path + " 2>/dev/null");
while (!resolved.empty() && std::isspace((unsigned char)resolved.back()))
resolved.pop_back();
if (resolved.empty()) continue;
bool matches = (resolved.find(slot) != std::string::npos);
if (!matches && slot.size() > 5 && slot[4] == ':') {
std::string short_addr = slot.substr(5);
matches = (resolved.find(short_addr) != std::string::npos);
}
if (!matches) continue;
// Found the card; check AMD VRAM
if (p.kv.find(K_VRAM_MB) == p.kv.end()) {
std::string vram_path = "/sys/class/drm/" + card_name + "/device/mem_info_vram_total";
FILE* vf = fopen(vram_path.c_str(), "r");
if (vf) {
uint64_t vram_bytes = 0;
fscanf(vf, "%llu", (unsigned long long*)&vram_bytes);
fclose(vf);
if (vram_bytes > 0) {
p.kv[K_VRAM_MB] = std::to_string(vram_bytes / (1024 * 1024));
}
}
}
// Enumerate connectors
std::string ls_out = run_cmd("ls /sys/class/drm/ 2>/dev/null");
if (!ls_out.empty()) {
std::istringstream lss(ls_out);
std::string entry;
while (lss >> entry) {
std::string prefix = card_name + "-";
if (entry.rfind(prefix, 0) != 0) continue;
std::string connector_name = entry.substr(prefix.size());
std::string status_path = "/sys/class/drm/" + entry + "/status";
FILE* sf = fopen(status_path.c_str(), "r");
std::string status = "unknown";
if (sf) {
char sbuf[32] = {};
fscanf(sf, "%31s", sbuf);
fclose(sf);
status = sbuf;
}
outputs.push_back(connector_name + ":" + status);
}
}
break;
}
if (!outputs.empty()) {
std::string out_str;
for (size_t oi = 0; oi < outputs.size(); ++oi) {
if (oi) out_str += LS;
out_str += outputs[oi];
}
p.kv[K_DISPLAY_OUTPUTS] = out_str;
}
result.push_back(std::move(p));
}
return result;
#endif
}

View file

@ -31,6 +31,7 @@ private:
std::vector<DiscoveredPart> discover_cpu_slots(); std::vector<DiscoveredPart> discover_cpu_slots();
std::vector<DiscoveredPart> discover_disks(); std::vector<DiscoveredPart> discover_disks();
std::vector<DiscoveredPart> discover_nics(); std::vector<DiscoveredPart> discover_nics();
std::vector<DiscoveredPart> discover_gpus();
// Run a shell command, return trimmed stdout (≤64 KB). "" on failure. // Run a shell command, return trimmed stdout (≤64 KB). "" on failure.
static std::string run_cmd(const std::string& cmd); static std::string run_cmd(const std::string& cmd);

View file

@ -245,7 +245,7 @@ static int cmd_discover_only(const std::string& filter_type) {
const std::vector<std::string> order = { const std::vector<std::string> order = {
PTYPE_CPU, PTYPE_CPU_SLOT, PTYPE_CPU, PTYPE_CPU_SLOT,
PTYPE_MEMORY_STICK, PTYPE_MEMORY_SLOT, PTYPE_MEMORY_STICK, PTYPE_MEMORY_SLOT,
PTYPE_DISK, PTYPE_NIC PTYPE_DISK, PTYPE_NIC, PTYPE_GPU
}; };
static const std::map<std::string, std::string> labels = { static const std::map<std::string, std::string> labels = {
{PTYPE_CPU, "CPUs"}, {PTYPE_CPU, "CPUs"},
@ -254,6 +254,7 @@ static int cmd_discover_only(const std::string& filter_type) {
{PTYPE_MEMORY_SLOT, "Memory Slots"}, {PTYPE_MEMORY_SLOT, "Memory Slots"},
{PTYPE_DISK, "Disks"}, {PTYPE_DISK, "Disks"},
{PTYPE_NIC, "NICs"}, {PTYPE_NIC, "NICs"},
{PTYPE_GPU, "GPUs"},
}; };
// Collect sections that actually have data // Collect sections that actually have data
@ -277,6 +278,7 @@ static int cmd_discover_only(const std::string& filter_type) {
bool item_last = (ii + 1 == items.size()); bool item_last = (ii + 1 == items.size());
auto& m = items[ii]->kv; auto& m = items[ii]->kv;
std::string line; std::string line;
std::vector<std::string> extra_lines;
if (type == PTYPE_CPU) { if (type == PTYPE_CPU) {
std::string sock = kv_get(m, K_SOCKET); std::string sock = kv_get(m, K_SOCKET);
@ -286,12 +288,46 @@ static int cmd_discover_only(const std::string& filter_type) {
std::string threads = kv_get(m, K_THREADS); std::string threads = kv_get(m, K_THREADS);
if (!sock.empty()) line += "[" + sock + "] "; if (!sock.empty()) line += "[" + sock + "] ";
line += name.empty() ? "(unknown)" : name; line += name.empty() ? "(unknown)" : name;
// Only append speed if the name doesn't already embed it (e.g. "@ 2.27GHz")
if (!speed.empty() && name.find('@') == std::string::npos) if (!speed.empty() && name.find('@') == std::string::npos)
line += " @ " + speed + " GHz"; line += " @ " + speed + " GHz";
if (!cores.empty() || !threads.empty()) if (!cores.empty() || !threads.empty())
line += " · " + (cores.empty() ? "?" : cores) line += " · " + (cores.empty() ? "?" : cores)
+ " cores / " + (threads.empty() ? "?" : threads) + " threads"; + " cores / " + (threads.empty() ? "?" : threads) + " threads";
// Secondary line: socket type, max speed, bus, voltage
{
std::string sock_type = kv_get(m, K_SOCKET_TYPE);
std::string max_spd = kv_get(m, K_MAX_SPEED_GHZ);
std::string bus = kv_get(m, K_BUS_MHZ);
std::string volt = kv_get(m, K_VOLTAGE_V);
std::string sline2;
if (!sock_type.empty()) sline2 += "Socket: " + sock_type;
if (!max_spd.empty() && max_spd != "0.000000") {
if (!sline2.empty()) sline2 += " · ";
sline2 += "Max: " + max_spd + " GHz";
}
if (!bus.empty() && bus != "0") {
if (!sline2.empty()) sline2 += " · ";
sline2 += "Bus: " + bus + " MHz";
}
if (!volt.empty() && volt != "0.0") {
if (!sline2.empty()) sline2 += " · ";
sline2 += "Voltage: " + volt + " V";
}
if (!sline2.empty()) extra_lines.push_back(sline2);
}
// Third line: cache
{
std::string l1 = kv_get(m, K_CACHE_L1_KB);
std::string l2 = kv_get(m, K_CACHE_L2_KB);
std::string l3 = kv_get(m, K_CACHE_L3_KB);
if (!l1.empty() || !l2.empty() || !l3.empty()) {
std::string cline = "Cache:";
if (!l1.empty() && l1 != "0") cline += " L1 " + l1 + " KB";
if (!l2.empty() && l2 != "0") cline += " L2 " + l2 + " KB";
if (!l3.empty() && l3 != "0") cline += " L3 " + l3 + " KB";
extra_lines.push_back(cline);
}
}
} else if (type == PTYPE_CPU_SLOT) { } else if (type == PTYPE_CPU_SLOT) {
std::string sock = kv_get(m, K_SOCKET); std::string sock = kv_get(m, K_SOCKET);
@ -304,6 +340,12 @@ static int cmd_discover_only(const std::string& filter_type) {
std::string size = kv_get(m, K_SIZE_MB); std::string size = kv_get(m, K_SIZE_MB);
std::string mfr = kv_get(m, K_MANUFACTURER); std::string mfr = kv_get(m, K_MANUFACTURER);
std::string speed = kv_get(m, K_SPEED_MHZ); std::string speed = kv_get(m, K_SPEED_MHZ);
std::string memtype = kv_get(m, K_MEM_TYPE);
std::string ff = kv_get(m, K_FORM_FACTOR);
std::string rank = kv_get(m, K_RANK);
std::string dw = kv_get(m, K_DATA_WIDTH);
std::string bw = kv_get(m, K_BANDWIDTH_MBPS);
std::string pn = kv_get(m, K_PART_NUMBER);
if (!loc.empty()) line += "[" + loc + "] "; if (!loc.empty()) line += "[" + loc + "] ";
if (!size.empty()) { if (!size.empty()) {
uint64_t mb = std::stoull(size); uint64_t mb = std::stoull(size);
@ -312,8 +354,19 @@ static int cmd_discover_only(const std::string& filter_type) {
else else
line += std::to_string(mb) + " MB"; line += std::to_string(mb) + " MB";
} }
if (!memtype.empty()) line += " " + memtype;
if (!ff.empty()) line += " " + ff;
if (!rank.empty() && rank != "0") line += " Rank " + rank;
if (!dw.empty() && dw != "0") line += " " + dw + "-bit";
if (!speed.empty() && speed != "0") line += " @ " + speed + " MHz";
if (!bw.empty() && bw != "0") {
char bwbuf[32];
uint64_t bwval = std::stoull(bw);
snprintf(bwbuf, sizeof(bwbuf), " \xe2\x89\x88 %llu MB/s", (unsigned long long)bwval);
line += bwbuf;
}
if (!mfr.empty()) line += " " + mfr; if (!mfr.empty()) line += " " + mfr;
if (!speed.empty()) line += " @ " + speed + " MHz"; if (!pn.empty()) line += " " + pn;
} else if (type == PTYPE_MEMORY_SLOT) { } else if (type == PTYPE_MEMORY_SLOT) {
std::string loc = kv_get(m, K_LOCATOR); std::string loc = kv_get(m, K_LOCATOR);
@ -360,12 +413,34 @@ static int cmd_discover_only(const std::string& filter_type) {
if (!ips.empty()) line += ls_join(ips) + " "; if (!ips.empty()) line += ls_join(ips) + " ";
if (!speed.empty() && speed != "0") if (!speed.empty() && speed != "0")
line += speed + " Mbps"; line += speed + " Mbps";
} else if (type == PTYPE_GPU) {
std::string mfr = kv_get(m, K_MANUFACTURER);
std::string model = kv_get(m, K_MODEL);
std::string serial = kv_get(m, K_SERIAL);
std::string vram = kv_get(m, K_VRAM_MB);
if (!mfr.empty()) line += mfr + " ";
if (!model.empty()) line += model;
if (!serial.empty()) line += " [" + serial + "]";
if (!vram.empty() && vram != "0") line += " " + vram + " MB VRAM";
// Outputs as secondary line
std::string outputs = kv_get(m, K_DISPLAY_OUTPUTS);
if (!outputs.empty())
extra_lines.push_back("Outputs: " + ls_join(outputs));
else
extra_lines.push_back("Outputs: (none)");
} }
// Trim trailing spaces // Trim trailing spaces
while (!line.empty() && line.back() == ' ') line.pop_back(); while (!line.empty() && line.back() == ' ') line.pop_back();
std::cout << child_pfx << (item_last ? "└── " : "├── ") << line << "\n"; std::cout << child_pfx << (item_last ? "└── " : "├── ") << line << "\n";
// Print extra lines (indented further)
std::string extra_pfx = child_pfx + (item_last ? " " : "") + " ";
for (auto& el : extra_lines) {
std::cout << extra_pfx << el << "\n";
}
} }
} }

View file

@ -49,6 +49,12 @@ struct MemoryStick : PartBase {
std::string channel_config; std::string channel_config;
std::string other_info; std::string other_info;
std::string locator; std::string locator;
std::string memory_type;
std::string form_factor;
std::string part_number;
uint32_t rank = 0;
uint32_t data_width_bits = 0;
uint64_t bandwidth_mbps = 0;
}; };
struct MemorySlot : PartBase { struct MemorySlot : PartBase {
@ -65,6 +71,13 @@ struct CPU : PartBase {
uint32_t cores = 0; uint32_t cores = 0;
uint32_t threads = 0; uint32_t threads = 0;
std::string socket_designation; std::string socket_designation;
std::string socket_type;
double max_speed_ghz = 0.0;
uint32_t bus_speed_mhz = 0;
uint32_t cache_l1_kb = 0;
uint32_t cache_l2_kb = 0;
uint32_t cache_l3_kb = 0;
double voltage_v = 0.0;
}; };
struct CPUSlot : PartBase { struct CPUSlot : PartBase {
@ -104,6 +117,14 @@ struct NetworkCard : PartBase {
bool dhcp = false; bool dhcp = false;
}; };
struct GPU : PartBase {
std::string manufacturer;
std::string model;
uint64_t vram_mb = 0;
uint64_t bandwidth_mbps = 0;
std::vector<std::string> display_outputs;
};
// ───────────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────────
// In-memory inventory // In-memory inventory
// ───────────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────────
@ -118,6 +139,7 @@ struct Inventory {
std::vector<CPUSlot> cpu_slots; std::vector<CPUSlot> cpu_slots;
std::vector<Disk> disks; std::vector<Disk> disks;
std::vector<NetworkCard> network_cards; std::vector<NetworkCard> network_cards;
std::vector<GPU> gpus;
uint32_t next_server_id = 1; uint32_t next_server_id = 1;
uint32_t next_part_type_id = 1; uint32_t next_part_type_id = 1;
@ -134,3 +156,4 @@ constexpr const char* PTYPE_CPU = "cpu";
constexpr const char* PTYPE_CPU_SLOT = "cpu_slot"; constexpr const char* PTYPE_CPU_SLOT = "cpu_slot";
constexpr const char* PTYPE_DISK = "disk"; constexpr const char* PTYPE_DISK = "disk";
constexpr const char* PTYPE_NIC = "nic"; constexpr const char* PTYPE_NIC = "nic";
constexpr const char* PTYPE_GPU = "gpu";

View file

@ -105,6 +105,15 @@ constexpr const char* K_SPEED_GHZ = "speed_ghz";
constexpr const char* K_CORES = "cores"; constexpr const char* K_CORES = "cores";
constexpr const char* K_THREADS = "threads"; constexpr const char* K_THREADS = "threads";
// CPU extended
constexpr const char* K_MAX_SPEED_GHZ = "max_speed_ghz";
constexpr const char* K_BUS_MHZ = "bus_speed_mhz";
constexpr const char* K_CACHE_L1_KB = "cache_l1_kb";
constexpr const char* K_CACHE_L2_KB = "cache_l2_kb";
constexpr const char* K_CACHE_L3_KB = "cache_l3_kb";
constexpr const char* K_VOLTAGE_V = "voltage_v";
constexpr const char* K_SOCKET_TYPE = "socket_type";
// CPUSlot // CPUSlot
constexpr const char* K_FORM_FACTOR = "form_factor"; constexpr const char* K_FORM_FACTOR = "form_factor";
constexpr const char* K_INSTALLED_CPU = "installed_cpu_id"; constexpr const char* K_INSTALLED_CPU = "installed_cpu_id";
@ -130,6 +139,18 @@ constexpr const char* K_MAC = "mac_address";
constexpr const char* K_IPS = "ip_addresses"; // LS-separated constexpr const char* K_IPS = "ip_addresses"; // LS-separated
constexpr const char* K_DHCP = "dhcp"; constexpr const char* K_DHCP = "dhcp";
// MemoryStick extended
constexpr const char* K_MEM_TYPE = "memory_type";
constexpr const char* K_PART_NUMBER = "part_number";
constexpr const char* K_RANK = "rank";
constexpr const char* K_DATA_WIDTH = "data_width_bits";
constexpr const char* K_BANDWIDTH_MBPS = "bandwidth_mbps";
// GPU
constexpr const char* K_VRAM_MB = "vram_mb";
constexpr const char* K_GPU_BANDWIDTH_MBPS = "gpu_bandwidth_mbps";
constexpr const char* K_DISPLAY_OUTPUTS = "display_outputs";
// ───────────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────────
// Helpers // Helpers
// ───────────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────────

View file

@ -110,6 +110,12 @@ static void apply_memory_stick(MemoryStick& m, const std::map<std::string,std::s
if (auto it = kv.find(K_CHANNEL_CONFIG); it != kv.end()) m.channel_config = it->second; if (auto it = kv.find(K_CHANNEL_CONFIG); it != kv.end()) m.channel_config = it->second;
if (auto it = kv.find(K_OTHER_INFO); it != kv.end()) m.other_info = it->second; if (auto it = kv.find(K_OTHER_INFO); it != kv.end()) m.other_info = it->second;
if (auto it = kv.find(K_LOCATOR); it != kv.end()) m.locator = it->second; if (auto it = kv.find(K_LOCATOR); it != kv.end()) m.locator = it->second;
if (auto it = kv.find(K_MEM_TYPE); it != kv.end()) m.memory_type = it->second;
if (auto it = kv.find(K_FORM_FACTOR); it != kv.end()) m.form_factor = it->second;
if (auto it = kv.find(K_PART_NUMBER); it != kv.end()) m.part_number = it->second;
if (auto it = kv.find(K_RANK); it != kv.end()) m.rank = static_cast<uint32_t>(std::stoul(it->second));
if (auto it = kv.find(K_DATA_WIDTH); it != kv.end()) m.data_width_bits = static_cast<uint32_t>(std::stoul(it->second));
if (auto it = kv.find(K_BANDWIDTH_MBPS); it != kv.end()) m.bandwidth_mbps = std::stoull(it->second);
} }
static void apply_memory_slot(MemorySlot& m, const std::map<std::string,std::string>& kv) { static void apply_memory_slot(MemorySlot& m, const std::map<std::string,std::string>& kv) {
@ -131,6 +137,13 @@ static void apply_cpu(CPU& c, const std::map<std::string,std::string>& kv) {
if (auto it = kv.find(K_CORES); it != kv.end()) c.cores = static_cast<uint32_t>(std::stoul(it->second)); if (auto it = kv.find(K_CORES); it != kv.end()) c.cores = static_cast<uint32_t>(std::stoul(it->second));
if (auto it = kv.find(K_THREADS); it != kv.end()) c.threads = static_cast<uint32_t>(std::stoul(it->second)); if (auto it = kv.find(K_THREADS); it != kv.end()) c.threads = static_cast<uint32_t>(std::stoul(it->second));
if (auto it = kv.find(K_SOCKET); it != kv.end()) c.socket_designation = it->second; if (auto it = kv.find(K_SOCKET); it != kv.end()) c.socket_designation = it->second;
if (auto it = kv.find(K_SOCKET_TYPE); it != kv.end()) c.socket_type = it->second;
if (auto it = kv.find(K_MAX_SPEED_GHZ); it != kv.end()) c.max_speed_ghz = std::stod(it->second);
if (auto it = kv.find(K_BUS_MHZ); it != kv.end()) c.bus_speed_mhz = static_cast<uint32_t>(std::stoul(it->second));
if (auto it = kv.find(K_CACHE_L1_KB); it != kv.end()) c.cache_l1_kb = static_cast<uint32_t>(std::stoul(it->second));
if (auto it = kv.find(K_CACHE_L2_KB); it != kv.end()) c.cache_l2_kb = static_cast<uint32_t>(std::stoul(it->second));
if (auto it = kv.find(K_CACHE_L3_KB); it != kv.end()) c.cache_l3_kb = static_cast<uint32_t>(std::stoul(it->second));
if (auto it = kv.find(K_VOLTAGE_V); it != kv.end()) c.voltage_v = std::stod(it->second);
} }
static void apply_cpu_slot(CPUSlot& c, const std::map<std::string,std::string>& kv) { static void apply_cpu_slot(CPUSlot& c, const std::map<std::string,std::string>& kv) {
@ -173,6 +186,15 @@ static void apply_nic(NetworkCard& n, const std::map<std::string,std::string>& k
if (auto it = kv.find(K_DHCP); it != kv.end()) n.dhcp = (it->second == "1" || it->second == "true"); if (auto it = kv.find(K_DHCP); it != kv.end()) n.dhcp = (it->second == "1" || it->second == "true");
} }
static void apply_gpu(GPU& g, const std::map<std::string,std::string>& kv) {
apply_base(g, kv);
if (auto it = kv.find(K_MANUFACTURER); it != kv.end()) g.manufacturer = it->second;
if (auto it = kv.find(K_MODEL); it != kv.end()) g.model = it->second;
if (auto it = kv.find(K_VRAM_MB); it != kv.end()) g.vram_mb = std::stoull(it->second);
if (auto it = kv.find(K_GPU_BANDWIDTH_MBPS); it != kv.end()) g.bandwidth_mbps = std::stoull(it->second);
if (auto it = kv.find(K_DISPLAY_OUTPUTS); it != kv.end()) g.display_outputs = ls_split(it->second);
}
// ───────────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────────
// Wire-row serializers // Wire-row serializers
// type_name<FS>part_id<FS>server_id<FS>serial_or_NULL<FS>last_updated<FS>k1<FS>v1... // type_name<FS>part_id<FS>server_id<FS>serial_or_NULL<FS>last_updated<FS>k1<FS>v1...
@ -193,6 +215,12 @@ std::string Database::serialize_memory_stick(const MemoryStick& m) const {
+ FS + K_CHANNEL_CONFIG + FS + m.channel_config + FS + K_CHANNEL_CONFIG + FS + m.channel_config
+ FS + K_OTHER_INFO + FS + m.other_info + FS + K_OTHER_INFO + FS + m.other_info
+ FS + K_LOCATOR + FS + m.locator + FS + K_LOCATOR + FS + m.locator
+ FS + K_MEM_TYPE + FS + m.memory_type
+ FS + K_FORM_FACTOR + FS + m.form_factor
+ FS + K_PART_NUMBER + FS + m.part_number
+ FS + K_RANK + FS + std::to_string(m.rank)
+ FS + K_DATA_WIDTH + FS + std::to_string(m.data_width_bits)
+ FS + K_BANDWIDTH_MBPS + FS + std::to_string(m.bandwidth_mbps)
+ "\n"; + "\n";
} }
@ -215,6 +243,13 @@ std::string Database::serialize_cpu(const CPU& c) const {
+ FS + K_CORES + FS + std::to_string(c.cores) + FS + K_CORES + FS + std::to_string(c.cores)
+ FS + K_THREADS + FS + std::to_string(c.threads) + FS + K_THREADS + FS + std::to_string(c.threads)
+ FS + K_SOCKET + FS + c.socket_designation + FS + K_SOCKET + FS + c.socket_designation
+ FS + K_SOCKET_TYPE + FS + c.socket_type
+ FS + K_MAX_SPEED_GHZ + FS + std::to_string(c.max_speed_ghz)
+ FS + K_BUS_MHZ + FS + std::to_string(c.bus_speed_mhz)
+ FS + K_CACHE_L1_KB + FS + std::to_string(c.cache_l1_kb)
+ FS + K_CACHE_L2_KB + FS + std::to_string(c.cache_l2_kb)
+ FS + K_CACHE_L3_KB + FS + std::to_string(c.cache_l3_kb)
+ FS + K_VOLTAGE_V + FS + std::to_string(c.voltage_v)
+ "\n"; + "\n";
} }
@ -286,6 +321,28 @@ std::string Database::serialize_nic(const NetworkCard& n) const {
+ "\n"; + "\n";
} }
std::string Database::serialize_gpu(const GPU& g) const {
auto esc_join = [](const std::vector<std::string>& v) -> std::string {
std::string out;
for (size_t i = 0; i < v.size(); ++i) {
if (i) out += LS;
for (char c : v[i]) {
if (c == '\\') out += "\\\\";
else if (c == '|') out += "\\|";
else out += c;
}
}
return out;
};
return base_prefix(PTYPE_GPU, g)
+ FS + K_MANUFACTURER + FS + g.manufacturer
+ FS + K_MODEL + FS + g.model
+ FS + K_VRAM_MB + FS + std::to_string(g.vram_mb)
+ FS + K_GPU_BANDWIDTH_MBPS + FS + std::to_string(g.bandwidth_mbps)
+ FS + K_DISPLAY_OUTPUTS + FS + esc_join(g.display_outputs)
+ "\n";
}
// ───────────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────────
// Constructor // Constructor
// ───────────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────────
@ -347,7 +404,13 @@ bool Database::save_nolock() {
<< "|" << escape(m.manufacturer) << "|" << escape(m.manufacturer)
<< "|" << escape(m.channel_config) << "|" << escape(m.channel_config)
<< "|" << escape(m.other_info) << "|" << escape(m.other_info)
<< "|" << escape(m.locator) << "\n"; << "|" << escape(m.locator)
<< "|" << escape(m.memory_type)
<< "|" << escape(m.form_factor)
<< "|" << escape(m.part_number)
<< "|" << m.rank
<< "|" << m.data_width_bits
<< "|" << m.bandwidth_mbps << "\n";
} }
for (const auto& m : inv_.memory_slots) { for (const auto& m : inv_.memory_slots) {
std::string inst = m.installed_stick_id.has_value() std::string inst = m.installed_stick_id.has_value()
@ -369,7 +432,14 @@ bool Database::save_nolock() {
<< "|" << c.speed_ghz << "|" << c.speed_ghz
<< "|" << c.cores << "|" << c.cores
<< "|" << c.threads << "|" << c.threads
<< "|" << escape(c.socket_designation) << "\n"; << "|" << escape(c.socket_designation)
<< "|" << escape(c.socket_type)
<< "|" << c.max_speed_ghz
<< "|" << c.bus_speed_mhz
<< "|" << c.cache_l1_kb
<< "|" << c.cache_l2_kb
<< "|" << c.cache_l3_kb
<< "|" << c.voltage_v << "\n";
} }
for (const auto& c : inv_.cpu_slots) { for (const auto& c : inv_.cpu_slots) {
std::string inst = c.installed_cpu_id.has_value() std::string inst = c.installed_cpu_id.has_value()
@ -423,6 +493,17 @@ bool Database::save_nolock() {
write_str_list(f, n.ip_addresses); write_str_list(f, n.ip_addresses);
f << "|" << (n.dhcp ? 1 : 0) << "\n"; f << "|" << (n.dhcp ? 1 : 0) << "\n";
} }
for (const auto& g : inv_.gpus) {
f << "GPU|" << g.part_id << "|" << g.server_id
<< "|" << escape(base_serial(g))
<< "|" << g.last_updated
<< "|" << escape(g.manufacturer)
<< "|" << escape(g.model)
<< "|" << g.vram_mb
<< "|" << g.bandwidth_mbps << "|";
write_str_list(f, g.display_outputs);
f << "\n";
}
return true; return true;
} }
@ -502,6 +583,12 @@ bool Database::load() {
m.channel_config = get(8); m.channel_config = get(8);
m.other_info = get(9); m.other_info = get(9);
m.locator = get(10); m.locator = get(10);
m.memory_type = get(11);
m.form_factor = get(12);
m.part_number = get(13);
m.rank = getu32(14);
m.data_width_bits = getu32(15);
m.bandwidth_mbps = getu64(16);
fresh.memory_sticks.push_back(std::move(m)); fresh.memory_sticks.push_back(std::move(m));
} else if (rec == "MSLOT" && flds.size() >= 8) { } else if (rec == "MSLOT" && flds.size() >= 8) {
MemorySlot m; MemorySlot m;
@ -526,6 +613,13 @@ bool Database::load() {
c.cores = getu32(8); c.cores = getu32(8);
c.threads = getu32(9); c.threads = getu32(9);
c.socket_designation = get(10); c.socket_designation = get(10);
c.socket_type = get(11);
c.max_speed_ghz = flds.size() > 12 && !flds[12].empty() ? std::stod(flds[12]) : 0.0;
c.bus_speed_mhz = getu32(13);
c.cache_l1_kb = getu32(14);
c.cache_l2_kb = getu32(15);
c.cache_l3_kb = getu32(16);
c.voltage_v = flds.size() > 17 && !flds[17].empty() ? std::stod(flds[17]) : 0.0;
fresh.cpus.push_back(std::move(c)); fresh.cpus.push_back(std::move(c));
} else if (rec == "CPUSLOT" && flds.size() >= 7) { } else if (rec == "CPUSLOT" && flds.size() >= 7) {
CPUSlot c; CPUSlot c;
@ -598,6 +692,22 @@ bool Database::load() {
} }
n.dhcp = (flds.size() > 12 && flds[12] == "1"); n.dhcp = (flds.size() > 12 && flds[12] == "1");
fresh.network_cards.push_back(std::move(n)); fresh.network_cards.push_back(std::move(n));
} else if (rec == "GPU" && flds.size() >= 9) {
GPU g;
g.part_id = getu32(1);
g.server_id = getu32(2);
g.serial_number = get_serial(3);
g.last_updated = geti64(4);
g.manufacturer = get(5);
g.model = get(6);
g.vram_mb = getu64(7);
g.bandwidth_mbps = getu64(8);
// display_outputs: LS separated in flds[9]
if (flds.size() > 9) {
for (auto& e : split(flds[9], LS))
g.display_outputs.push_back(unescape(e));
}
fresh.gpus.push_back(std::move(g));
} }
} }
inv_ = std::move(fresh); inv_ = std::move(fresh);
@ -739,6 +849,10 @@ uint32_t Database::add_part_nolock(const std::string& type_name, uint32_t server
NetworkCard n; n.part_id = pid; n.server_id = server_id; n.last_updated = now; NetworkCard n; n.part_id = pid; n.server_id = server_id; n.last_updated = now;
apply_nic(n, kv); apply_nic(n, kv);
inv_.network_cards.push_back(std::move(n)); inv_.network_cards.push_back(std::move(n));
} else if (type_name == PTYPE_GPU) {
GPU g; g.part_id = pid; g.server_id = server_id; g.last_updated = now;
apply_gpu(g, kv);
inv_.gpus.push_back(std::move(g));
} else { } else {
// Unknown type — no-op, reclaim the id by decrementing // Unknown type — no-op, reclaim the id by decrementing
--inv_.next_part_id; --inv_.next_part_id;
@ -796,6 +910,8 @@ uint32_t Database::upsert_part(const std::string& type_name, uint32_t server_id,
apply_cpu_slot(p, kv); apply_cpu_slot(p, kv);
else if constexpr (std::is_same_v<std::decay_t<decltype(p)>, Disk>) else if constexpr (std::is_same_v<std::decay_t<decltype(p)>, Disk>)
apply_disk(p, kv); apply_disk(p, kv);
else if constexpr (std::is_same_v<std::decay_t<decltype(p)>, GPU>)
apply_gpu(p, kv);
p.last_updated = now; p.last_updated = now;
save_nolock(); save_nolock();
return p.part_id; return p.part_id;
@ -810,6 +926,7 @@ uint32_t Database::upsert_part(const std::string& type_name, uint32_t server_id,
else if (type_name == PTYPE_CPU) found = try_update(inv_.cpus); else if (type_name == PTYPE_CPU) found = try_update(inv_.cpus);
else if (type_name == PTYPE_CPU_SLOT) found = try_update(inv_.cpu_slots); else if (type_name == PTYPE_CPU_SLOT) found = try_update(inv_.cpu_slots);
else if (type_name == PTYPE_DISK) found = try_update(inv_.disks); else if (type_name == PTYPE_DISK) found = try_update(inv_.disks);
else if (type_name == PTYPE_GPU) found = try_update(inv_.gpus);
if (found) return found; if (found) return found;
} }
// Natural key fallback when no serial — prevents duplicate inserts on each run // Natural key fallback when no serial — prevents duplicate inserts on each run
@ -866,6 +983,22 @@ uint32_t Database::upsert_part(const std::string& type_name, uint32_t server_id,
} }
} }
} }
} else if (type_name == PTYPE_GPU) {
auto mfr_it = kv.find(K_MANUFACTURER);
auto mod_it = kv.find(K_MODEL);
if (mod_it != kv.end() && !mod_it->second.empty()) {
const std::string& model = mod_it->second;
const std::string& mfr = (mfr_it != kv.end()) ? mfr_it->second : "";
for (auto& g : inv_.gpus) {
if (g.server_id == server_id && g.model == model &&
(mfr.empty() || g.manufacturer == mfr)) {
apply_gpu(g, kv);
g.last_updated = now;
save_nolock();
return g.part_id;
}
}
}
} }
} }
// Not found — insert // Not found — insert
@ -896,6 +1029,7 @@ bool Database::edit_part(uint32_t part_id, const std::map<std::string,std::strin
if (try_edit(inv_.cpu_slots, apply_cpu_slot)) return true; if (try_edit(inv_.cpu_slots, apply_cpu_slot)) return true;
if (try_edit(inv_.disks, apply_disk)) return true; if (try_edit(inv_.disks, apply_disk)) return true;
if (try_edit(inv_.network_cards, apply_nic)) return true; if (try_edit(inv_.network_cards, apply_nic)) return true;
if (try_edit(inv_.gpus, apply_gpu)) return true;
return false; return false;
} }
@ -918,7 +1052,8 @@ bool Database::remove_part(uint32_t part_id) {
|| try_remove(inv_.cpus) || try_remove(inv_.cpus)
|| try_remove(inv_.cpu_slots) || try_remove(inv_.cpu_slots)
|| try_remove(inv_.disks) || try_remove(inv_.disks)
|| try_remove(inv_.network_cards); || try_remove(inv_.network_cards)
|| try_remove(inv_.gpus);
if (removed) save_nolock(); if (removed) save_nolock();
return removed; return removed;
} }
@ -956,6 +1091,10 @@ std::vector<std::string> Database::list_parts(uint32_t server_id,
for (const auto& n : inv_.network_cards) for (const auto& n : inv_.network_cards)
if (n.server_id == server_id) rows.push_back(serialize_nic(n)); if (n.server_id == server_id) rows.push_back(serialize_nic(n));
if (all || type_filter == PTYPE_GPU)
for (const auto& g : inv_.gpus)
if (g.server_id == server_id) rows.push_back(serialize_gpu(g));
return rows; return rows;
} }
@ -974,6 +1113,8 @@ std::string Database::get_part_row(uint32_t part_id) const {
if (d.part_id == part_id) return serialize_disk(d); if (d.part_id == part_id) return serialize_disk(d);
for (const auto& n : inv_.network_cards) for (const auto& n : inv_.network_cards)
if (n.part_id == part_id) return serialize_nic(n); if (n.part_id == part_id) return serialize_nic(n);
for (const auto& g : inv_.gpus)
if (g.part_id == part_id) return serialize_gpu(g);
return ""; return "";
} }

View file

@ -95,4 +95,5 @@ private:
std::string serialize_cpu_slot (const CPUSlot& c) const; std::string serialize_cpu_slot (const CPUSlot& c) const;
std::string serialize_disk (const Disk& d) const; std::string serialize_disk (const Disk& d) const;
std::string serialize_nic (const NetworkCard& n) const; std::string serialize_nic (const NetworkCard& n) const;
std::string serialize_gpu (const GPU& g) const;
}; };