diff --git a/orchestration/ansible/roles/inventory-cli/files/inventory-cli b/orchestration/ansible/roles/inventory-cli/files/inventory-cli index 2df9e62..67cbe89 100755 Binary files a/orchestration/ansible/roles/inventory-cli/files/inventory-cli and b/orchestration/ansible/roles/inventory-cli/files/inventory-cli differ diff --git a/services/device-inventory/src/client/discovery.cpp b/services/device-inventory/src/client/discovery.cpp index 008e3f6..2f58eb6 100644 --- a/services/device-inventory/src/client/discovery.cpp +++ b/services/device-inventory/src/client/discovery.cpp @@ -291,7 +291,8 @@ std::vector Discovery::discover_all() { std::vector all; for (auto& v : {discover_memory_sticks(), discover_memory_slots(), discover_cpus(), discover_cpu_slots(), - discover_disks(), discover_nics()}) { + discover_disks(), discover_nics(), + discover_gpus()}) { all.insert(all.end(), v.begin(), v.end()); } return all; @@ -304,6 +305,7 @@ std::vector Discovery::discover(const std::string& type_name) { 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(); + if (type_name == PTYPE_GPU) return discover_gpus(); std::cerr << "discover: unknown type '" << type_name << "'\n"; return {}; } @@ -423,6 +425,39 @@ std::vector Discovery::discover_memory_sticks() { if (b.count("Locator") && !b["Locator"].empty()) 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(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(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)); } #endif @@ -604,8 +639,193 @@ std::vector Discovery::discover_cpus() { if (b.count("Socket Designation") && b["Socket Designation"] != "Not Specified") p.kv[K_SOCKET] = b["Socket Designation"]; + // Extended CPU fields from dmidecode type 4 + if (b.count("Max Speed")) { + double max_mhz = static_cast(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)); } + + // Enrich CPUs with L1/L2/L3 cache sizes from sysfs + // Build a map: physical_package_id → first cpu index + { + std::map 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(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> by_phys; + std::map 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(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 " 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 return result; @@ -1199,3 +1419,162 @@ std::vector Discovery::discover_nics() { return result; } + +// ═════════════════════════════════════════════════════════════════════════════ +// GPUs +// ═════════════════════════════════════════════════════════════════════════════ + +std::vector Discovery::discover_gpus() { + std::vector 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::vector 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 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 +} diff --git a/services/device-inventory/src/client/discovery.h b/services/device-inventory/src/client/discovery.h index 0b98a13..736573a 100644 --- a/services/device-inventory/src/client/discovery.h +++ b/services/device-inventory/src/client/discovery.h @@ -31,6 +31,7 @@ private: std::vector discover_cpu_slots(); std::vector discover_disks(); std::vector discover_nics(); + std::vector discover_gpus(); // Run a shell command, return trimmed stdout (≤64 KB). "" on failure. static std::string run_cmd(const std::string& cmd); diff --git a/services/device-inventory/src/client/main.cpp b/services/device-inventory/src/client/main.cpp index b6f991f..69713ac 100644 --- a/services/device-inventory/src/client/main.cpp +++ b/services/device-inventory/src/client/main.cpp @@ -245,7 +245,7 @@ static int cmd_discover_only(const std::string& filter_type) { const std::vector order = { PTYPE_CPU, PTYPE_CPU_SLOT, PTYPE_MEMORY_STICK, PTYPE_MEMORY_SLOT, - PTYPE_DISK, PTYPE_NIC + PTYPE_DISK, PTYPE_NIC, PTYPE_GPU }; static const std::map labels = { {PTYPE_CPU, "CPUs"}, @@ -254,6 +254,7 @@ static int cmd_discover_only(const std::string& filter_type) { {PTYPE_MEMORY_SLOT, "Memory Slots"}, {PTYPE_DISK, "Disks"}, {PTYPE_NIC, "NICs"}, + {PTYPE_GPU, "GPUs"}, }; // 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()); auto& m = items[ii]->kv; std::string line; + std::vector extra_lines; if (type == PTYPE_CPU) { 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); if (!sock.empty()) line += "[" + sock + "] "; 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) line += " @ " + speed + " GHz"; if (!cores.empty() || !threads.empty()) line += " · " + (cores.empty() ? "?" : cores) + " 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) { std::string sock = kv_get(m, K_SOCKET); @@ -300,10 +336,16 @@ static int cmd_discover_only(const std::string& filter_type) { if (!name.empty()) line += " " + name; } else if (type == PTYPE_MEMORY_STICK) { - std::string loc = kv_get(m, K_LOCATOR); - std::string size = kv_get(m, K_SIZE_MB); - std::string mfr = kv_get(m, K_MANUFACTURER); - std::string speed = kv_get(m, K_SPEED_MHZ); + std::string loc = kv_get(m, K_LOCATOR); + std::string size = kv_get(m, K_SIZE_MB); + std::string mfr = kv_get(m, K_MANUFACTURER); + 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 (!size.empty()) { uint64_t mb = std::stoull(size); @@ -312,8 +354,19 @@ static int cmd_discover_only(const std::string& filter_type) { else line += std::to_string(mb) + " MB"; } - if (!mfr.empty()) line += " " + mfr; - if (!speed.empty()) line += " @ " + speed + " MHz"; + 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 (!pn.empty()) line += " " + pn; } else if (type == PTYPE_MEMORY_SLOT) { 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 (!speed.empty() && speed != "0") 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 while (!line.empty() && line.back() == ' ') line.pop_back(); 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"; + } } } diff --git a/services/device-inventory/src/common/models.h b/services/device-inventory/src/common/models.h index 28c4833..82bbf77 100644 --- a/services/device-inventory/src/common/models.h +++ b/services/device-inventory/src/common/models.h @@ -49,6 +49,12 @@ struct MemoryStick : PartBase { std::string channel_config; std::string other_info; 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 { @@ -65,6 +71,13 @@ struct CPU : PartBase { uint32_t cores = 0; uint32_t threads = 0; 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 { @@ -104,6 +117,14 @@ struct NetworkCard : PartBase { bool dhcp = false; }; +struct GPU : PartBase { + std::string manufacturer; + std::string model; + uint64_t vram_mb = 0; + uint64_t bandwidth_mbps = 0; + std::vector display_outputs; +}; + // ───────────────────────────────────────────────────────────────────────────── // In-memory inventory // ───────────────────────────────────────────────────────────────────────────── @@ -118,6 +139,7 @@ struct Inventory { std::vector cpu_slots; std::vector disks; std::vector network_cards; + std::vector gpus; uint32_t next_server_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_DISK = "disk"; constexpr const char* PTYPE_NIC = "nic"; +constexpr const char* PTYPE_GPU = "gpu"; diff --git a/services/device-inventory/src/common/protocol.h b/services/device-inventory/src/common/protocol.h index 7160c68..69e1aac 100644 --- a/services/device-inventory/src/common/protocol.h +++ b/services/device-inventory/src/common/protocol.h @@ -105,6 +105,15 @@ constexpr const char* K_SPEED_GHZ = "speed_ghz"; constexpr const char* K_CORES = "cores"; 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 constexpr const char* K_FORM_FACTOR = "form_factor"; 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_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 // ───────────────────────────────────────────────────────────────────────────── diff --git a/services/device-inventory/src/server/database.cpp b/services/device-inventory/src/server/database.cpp index 301c1d8..87188af 100644 --- a/services/device-inventory/src/server/database.cpp +++ b/services/device-inventory/src/server/database.cpp @@ -110,6 +110,12 @@ static void apply_memory_stick(MemoryStick& m, const std::mapsecond; 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_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(std::stoul(it->second)); + if (auto it = kv.find(K_DATA_WIDTH); it != kv.end()) m.data_width_bits = static_cast(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& kv) { @@ -131,6 +137,13 @@ static void apply_cpu(CPU& c, const std::map& kv) { if (auto it = kv.find(K_CORES); it != kv.end()) c.cores = static_cast(std::stoul(it->second)); if (auto it = kv.find(K_THREADS); it != kv.end()) c.threads = static_cast(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_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(std::stoul(it->second)); + if (auto it = kv.find(K_CACHE_L1_KB); it != kv.end()) c.cache_l1_kb = static_cast(std::stoul(it->second)); + if (auto it = kv.find(K_CACHE_L2_KB); it != kv.end()) c.cache_l2_kb = static_cast(std::stoul(it->second)); + if (auto it = kv.find(K_CACHE_L3_KB); it != kv.end()) c.cache_l3_kb = static_cast(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& kv) { @@ -173,6 +186,15 @@ static void apply_nic(NetworkCard& n, const std::map& k 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& 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 // type_namepart_idserver_idserial_or_NULLlast_updatedk1v1... @@ -193,6 +215,12 @@ std::string Database::serialize_memory_stick(const MemoryStick& m) const { + FS + K_CHANNEL_CONFIG + FS + m.channel_config + FS + K_OTHER_INFO + FS + m.other_info + 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"; } @@ -214,7 +242,14 @@ std::string Database::serialize_cpu(const CPU& c) const { + FS + K_SPEED_GHZ + FS + std::to_string(c.speed_ghz) + FS + K_CORES + FS + std::to_string(c.cores) + 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"; } @@ -286,6 +321,28 @@ std::string Database::serialize_nic(const NetworkCard& n) const { + "\n"; } +std::string Database::serialize_gpu(const GPU& g) const { + auto esc_join = [](const std::vector& 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 // ───────────────────────────────────────────────────────────────────────────── @@ -347,7 +404,13 @@ bool Database::save_nolock() { << "|" << escape(m.manufacturer) << "|" << escape(m.channel_config) << "|" << 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) { std::string inst = m.installed_stick_id.has_value() @@ -369,7 +432,14 @@ bool Database::save_nolock() { << "|" << c.speed_ghz << "|" << c.cores << "|" << 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) { std::string inst = c.installed_cpu_id.has_value() @@ -423,6 +493,17 @@ bool Database::save_nolock() { write_str_list(f, n.ip_addresses); 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; } @@ -502,6 +583,12 @@ bool Database::load() { m.channel_config = get(8); m.other_info = get(9); 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)); } else if (rec == "MSLOT" && flds.size() >= 8) { MemorySlot m; @@ -526,6 +613,13 @@ bool Database::load() { c.cores = getu32(8); c.threads = getu32(9); 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)); } else if (rec == "CPUSLOT" && flds.size() >= 7) { CPUSlot c; @@ -598,6 +692,22 @@ bool Database::load() { } n.dhcp = (flds.size() > 12 && flds[12] == "1"); 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); @@ -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; apply_nic(n, kv); 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 { // Unknown type — no-op, reclaim the id by decrementing --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); else if constexpr (std::is_same_v, Disk>) apply_disk(p, kv); + else if constexpr (std::is_same_v, GPU>) + apply_gpu(p, kv); p.last_updated = now; save_nolock(); 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_SLOT) found = try_update(inv_.cpu_slots); 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; } // 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 @@ -896,6 +1029,7 @@ bool Database::edit_part(uint32_t part_id, const std::map Database::list_parts(uint32_t server_id, for (const auto& n : inv_.network_cards) 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; } @@ -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); for (const auto& n : inv_.network_cards) 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 ""; } diff --git a/services/device-inventory/src/server/database.h b/services/device-inventory/src/server/database.h index d0fb7c2..0826c36 100644 --- a/services/device-inventory/src/server/database.h +++ b/services/device-inventory/src/server/database.h @@ -95,4 +95,5 @@ private: std::string serialize_cpu_slot (const CPUSlot& c) const; std::string serialize_disk (const Disk& d) const; std::string serialize_nic (const NetworkCard& n) const; + std::string serialize_gpu (const GPU& g) const; };