feat(device-inventory): hardware discovery pipeline natural key deduplication

- 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>
This commit is contained in:
Dan V 2026-03-31 22:37:17 +02:00
parent 12b62bea00
commit 6e1e4b4134
4 changed files with 173 additions and 20 deletions

View file

@ -420,6 +420,9 @@ std::vector<DiscoveredPart> Discovery::discover_memory_sticks() {
} }
if (!other.empty()) p.kv[K_OTHER_INFO] = other; 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)); result.push_back(std::move(p));
} }
#endif #endif
@ -500,6 +503,8 @@ std::vector<DiscoveredPart> Discovery::discover_memory_slots() {
max_size_mb / static_cast<uint64_t>(blocks17.size())); max_size_mb / static_cast<uint64_t>(blocks17.size()));
p.kv[K_INSTALLED_STICK] = "NULL"; 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)); result.push_back(std::move(p));
} }
#endif #endif
@ -567,9 +572,9 @@ std::vector<DiscoveredPart> Discovery::discover_cpus() {
} }
for (auto& b : blocks) { for (auto& b : blocks) {
// Skip unpopulated sockets // Skip phantom entries (iLO processor, "Other" type, absent Status)
if (b.count("Status") && b["Status"].find("Unpopulated") != std::string::npos) if (!b.count("Status") || b["Status"].find("Populated") == std::string::npos) continue;
continue; if (b.count("Type") && b["Type"].find("Central Processor") == std::string::npos) continue;
DiscoveredPart p; DiscoveredPart p;
p.type_name = PTYPE_CPU; p.type_name = PTYPE_CPU;
@ -592,6 +597,9 @@ std::vector<DiscoveredPart> Discovery::discover_cpus() {
if (b.count("Core Count")) p.kv[K_CORES] = b["Core Count"]; 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("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)); result.push_back(std::move(p));
} }
#endif #endif
@ -625,11 +633,22 @@ std::vector<DiscoveredPart> Discovery::discover_cpu_slots() {
} }
for (auto& b : blocks) { 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; DiscoveredPart p;
p.type_name = PTYPE_CPU_SLOT; p.type_name = PTYPE_CPU_SLOT;
if (b.count("Upgrade")) p.kv[K_FORM_FACTOR] = b["Upgrade"]; if (b.count("Upgrade")) p.kv[K_FORM_FACTOR] = b["Upgrade"];
else p.kv[K_FORM_FACTOR] = "unknown"; else p.kv[K_FORM_FACTOR] = "unknown";
p.kv[K_INSTALLED_CPU] = "NULL"; 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)); result.push_back(std::move(p));
} }
#endif #endif
@ -819,6 +838,8 @@ std::vector<DiscoveredPart> Discovery::discover_disks() {
std::string type = json_str(dev, "type"); std::string type = json_str(dev, "type");
if (type != "disk") continue; if (type != "disk") continue;
std::string dev_name = json_str(dev, "name");
DiscoveredPart p; DiscoveredPart p;
p.type_name = PTYPE_DISK; p.type_name = PTYPE_DISK;
@ -877,15 +898,33 @@ std::vector<DiscoveredPart> Discovery::discover_disks() {
p.kv[K_VM_HOSTNAMES] = ""; p.kv[K_VM_HOSTNAMES] = "";
p.kv[K_VM_SERVER_IDS] = ""; 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)); result.push_back(std::move(p));
} }
#endif #endif
return result; return result;
} }
// ═════════════════════════════════════════════════════════════════════════════
// NICs
// ═════════════════════════════════════════════════════════════════════════════ // ═════════════════════════════════════════════════════════════════════════════
std::vector<DiscoveredPart> Discovery::discover_nics() { std::vector<DiscoveredPart> Discovery::discover_nics() {
@ -1042,19 +1081,49 @@ std::vector<DiscoveredPart> Discovery::discover_nics() {
std::string flags_str= json_str(iface, "flags"); // may be array std::string flags_str= json_str(iface, "flags"); // may be array
if (ifname.empty()) continue; if (ifname.empty()) continue;
// Skip loopback and virtual interfaces // Only include physical NICs: those with a /device symlink pointing to a PCI entry.
if (ifname == "lo") continue; // Virtual interfaces (bridges, veth, flannel, fwbr, vmbr, tap, etc.) have no device symlink.
if (ifname.rfind("docker", 0) == 0 || {
ifname.rfind("veth", 0) == 0 || std::string dev_path = "/sys/class/net/" + ifname + "/device";
ifname.rfind("virbr", 0) == 0 || struct stat st;
ifname.rfind("br-", 0) == 0 || if (lstat(dev_path.c_str(), &st) != 0 || !S_ISLNK(st.st_mode)) continue;
ifname.rfind("tun", 0) == 0 || }
ifname.rfind("tap", 0) == 0) 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; DiscoveredPart p;
p.type_name = PTYPE_NIC; p.type_name = PTYPE_NIC;
p.kv[K_MAC] = mac; p.kv[K_MAC] = mac;
p.kv[K_MODEL] = link_type; p.kv[K_MODEL] = pci_model.empty() ? link_type : pci_model;
if (!pci_vendor.empty()) p.kv[K_MANUFACTURER] = pci_vendor;
// connection type // connection type
std::string lower_ifname = ifname; std::string lower_ifname = ifname;

View file

@ -48,12 +48,14 @@ struct MemoryStick : PartBase {
std::string manufacturer; std::string manufacturer;
std::string channel_config; std::string channel_config;
std::string other_info; std::string other_info;
std::string locator;
}; };
struct MemorySlot : PartBase { struct MemorySlot : PartBase {
uint32_t allowed_speed_mhz = 0; uint32_t allowed_speed_mhz = 0;
uint64_t allowed_size_mb = 0; uint64_t allowed_size_mb = 0;
std::optional<uint32_t> installed_stick_id; // ref to MemoryStick.part_id std::optional<uint32_t> installed_stick_id; // ref to MemoryStick.part_id
std::string locator;
}; };
struct CPU : PartBase { struct CPU : PartBase {
@ -62,11 +64,13 @@ struct CPU : PartBase {
double speed_ghz = 0.0; double speed_ghz = 0.0;
uint32_t cores = 0; uint32_t cores = 0;
uint32_t threads = 0; uint32_t threads = 0;
std::string socket_designation;
}; };
struct CPUSlot : PartBase { struct CPUSlot : PartBase {
std::string form_factor; // "LGA1700", "AM5", … std::string form_factor; // "LGA1700", "AM5", …
std::optional<uint32_t> installed_cpu_id; // ref to CPU.part_id std::optional<uint32_t> installed_cpu_id; // ref to CPU.part_id
std::string socket_designation;
}; };
// connection_type: "SATA" | "NVMe" | "SAS" | "USB" | "virtual" | … // connection_type: "SATA" | "NVMe" | "SAS" | "USB" | "virtual" | …
@ -85,6 +89,7 @@ struct Disk : PartBase {
std::vector<uint64_t> partition_sizes_gb; std::vector<uint64_t> partition_sizes_gb;
std::vector<std::string> vm_hostnames; // hosts/VMs with disk access std::vector<std::string> vm_hostnames; // hosts/VMs with disk access
std::vector<uint32_t> vm_server_ids; // inventory server ids std::vector<uint32_t> vm_server_ids; // inventory server ids
std::string device_name;
}; };
// connection_type: "ethernet" | "wifi" | "infiniband" | … // connection_type: "ethernet" | "wifi" | "infiniband" | …

View file

@ -82,6 +82,11 @@ constexpr const char* RESP_END = "END";
constexpr const char* K_SERIAL = "serial"; constexpr const char* K_SERIAL = "serial";
constexpr const char* K_LAST_UPDATED = "last_updated"; constexpr const char* K_LAST_UPDATED = "last_updated";
// Natural keys for deduplication when serial is absent
constexpr const char* K_LOCATOR = "locator"; // DIMM locator for memory
constexpr const char* K_SOCKET = "socket_designation"; // CPU socket label
constexpr const char* K_DEVICE_NAME = "device_name"; // block device name (sda, nvme0n1…)
// MemoryStick // MemoryStick
constexpr const char* K_SPEED_MHZ = "speed_mhz"; constexpr const char* K_SPEED_MHZ = "speed_mhz";
constexpr const char* K_SIZE_MB = "size_mb"; constexpr const char* K_SIZE_MB = "size_mb";

View file

@ -109,6 +109,7 @@ static void apply_memory_stick(MemoryStick& m, const std::map<std::string,std::s
if (auto it = kv.find(K_MANUFACTURER); it != kv.end()) m.manufacturer = it->second; if (auto it = kv.find(K_MANUFACTURER); it != kv.end()) m.manufacturer = it->second;
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;
} }
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) {
@ -119,6 +120,7 @@ static void apply_memory_slot(MemorySlot& m, const std::map<std::string,std::str
if (it->second == "NULL") m.installed_stick_id = std::nullopt; if (it->second == "NULL") m.installed_stick_id = std::nullopt;
else m.installed_stick_id = static_cast<uint32_t>(std::stoul(it->second)); else m.installed_stick_id = static_cast<uint32_t>(std::stoul(it->second));
} }
if (auto it = kv.find(K_LOCATOR); it != kv.end()) m.locator = it->second;
} }
static void apply_cpu(CPU& c, const std::map<std::string,std::string>& kv) { static void apply_cpu(CPU& c, const std::map<std::string,std::string>& kv) {
@ -128,6 +130,7 @@ static void apply_cpu(CPU& c, const std::map<std::string,std::string>& kv) {
if (auto it = kv.find(K_SPEED_GHZ); it != kv.end()) c.speed_ghz = std::stod(it->second); if (auto it = kv.find(K_SPEED_GHZ); it != kv.end()) c.speed_ghz = std::stod(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_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;
} }
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) {
@ -137,6 +140,7 @@ static void apply_cpu_slot(CPUSlot& c, const std::map<std::string,std::string>&
if (it->second == "NULL") c.installed_cpu_id = std::nullopt; if (it->second == "NULL") c.installed_cpu_id = std::nullopt;
else c.installed_cpu_id = static_cast<uint32_t>(std::stoul(it->second)); else c.installed_cpu_id = static_cast<uint32_t>(std::stoul(it->second));
} }
if (auto it = kv.find(K_SOCKET); it != kv.end()) c.socket_designation = it->second;
} }
static void apply_disk(Disk& d, const std::map<std::string,std::string>& kv) { static void apply_disk(Disk& d, const std::map<std::string,std::string>& kv) {
@ -154,6 +158,7 @@ static void apply_disk(Disk& d, const std::map<std::string,std::string>& kv) {
if (auto it = kv.find(K_PARTITION_SIZES); it != kv.end()) d.partition_sizes_gb = ls_split_u64(it->second); if (auto it = kv.find(K_PARTITION_SIZES); it != kv.end()) d.partition_sizes_gb = ls_split_u64(it->second);
if (auto it = kv.find(K_VM_HOSTNAMES); it != kv.end()) d.vm_hostnames = ls_split(it->second); if (auto it = kv.find(K_VM_HOSTNAMES); it != kv.end()) d.vm_hostnames = ls_split(it->second);
if (auto it = kv.find(K_VM_SERVER_IDS); it != kv.end()) d.vm_server_ids = ls_split_u32(it->second); if (auto it = kv.find(K_VM_SERVER_IDS); it != kv.end()) d.vm_server_ids = ls_split_u32(it->second);
if (auto it = kv.find(K_DEVICE_NAME); it != kv.end()) d.device_name = it->second;
} }
static void apply_nic(NetworkCard& n, const std::map<std::string,std::string>& kv) { static void apply_nic(NetworkCard& n, const std::map<std::string,std::string>& kv) {
@ -187,6 +192,7 @@ std::string Database::serialize_memory_stick(const MemoryStick& m) const {
+ FS + K_MANUFACTURER + FS + m.manufacturer + FS + K_MANUFACTURER + FS + m.manufacturer
+ 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
+ "\n"; + "\n";
} }
@ -197,6 +203,7 @@ std::string Database::serialize_memory_slot(const MemorySlot& m) const {
+ FS + K_ALLOWED_SPEED + FS + std::to_string(m.allowed_speed_mhz) + FS + K_ALLOWED_SPEED + FS + std::to_string(m.allowed_speed_mhz)
+ FS + K_ALLOWED_SIZE + FS + std::to_string(m.allowed_size_mb) + FS + K_ALLOWED_SIZE + FS + std::to_string(m.allowed_size_mb)
+ FS + K_INSTALLED_STICK + FS + installed + FS + K_INSTALLED_STICK + FS + installed
+ FS + K_LOCATOR + FS + m.locator
+ "\n"; + "\n";
} }
@ -207,6 +214,7 @@ std::string Database::serialize_cpu(const CPU& c) const {
+ FS + K_SPEED_GHZ + FS + std::to_string(c.speed_ghz) + FS + K_SPEED_GHZ + FS + std::to_string(c.speed_ghz)
+ 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
+ "\n"; + "\n";
} }
@ -216,6 +224,7 @@ std::string Database::serialize_cpu_slot(const CPUSlot& c) const {
return base_prefix(PTYPE_CPU_SLOT, c) return base_prefix(PTYPE_CPU_SLOT, c)
+ FS + K_FORM_FACTOR + FS + c.form_factor + FS + K_FORM_FACTOR + FS + c.form_factor
+ FS + K_INSTALLED_CPU + FS + installed + FS + K_INSTALLED_CPU + FS + installed
+ FS + K_SOCKET + FS + c.socket_designation
+ "\n"; + "\n";
} }
@ -248,6 +257,7 @@ std::string Database::serialize_disk(const Disk& d) const {
+ FS + K_PARTITION_SIZES + FS + ls_join_u64(d.partition_sizes_gb) + FS + K_PARTITION_SIZES + FS + ls_join_u64(d.partition_sizes_gb)
+ FS + K_VM_HOSTNAMES + FS + esc_join(d.vm_hostnames) + FS + K_VM_HOSTNAMES + FS + esc_join(d.vm_hostnames)
+ FS + K_VM_SERVER_IDS + FS + ls_join_u32(d.vm_server_ids) + FS + K_VM_SERVER_IDS + FS + ls_join_u32(d.vm_server_ids)
+ FS + K_DEVICE_NAME + FS + d.device_name
+ "\n"; + "\n";
} }
@ -336,7 +346,8 @@ bool Database::save_nolock() {
<< "|" << m.size_mb << "|" << m.size_mb
<< "|" << escape(m.manufacturer) << "|" << escape(m.manufacturer)
<< "|" << escape(m.channel_config) << "|" << escape(m.channel_config)
<< "|" << escape(m.other_info) << "\n"; << "|" << escape(m.other_info)
<< "|" << escape(m.locator) << "\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()
@ -346,7 +357,8 @@ bool Database::save_nolock() {
<< "|" << m.last_updated << "|" << m.last_updated
<< "|" << m.allowed_speed_mhz << "|" << m.allowed_speed_mhz
<< "|" << m.allowed_size_mb << "|" << m.allowed_size_mb
<< "|" << inst << "\n"; << "|" << inst
<< "|" << escape(m.locator) << "\n";
} }
for (const auto& c : inv_.cpus) { for (const auto& c : inv_.cpus) {
f << "CPU|" << c.part_id << "|" << c.server_id f << "CPU|" << c.part_id << "|" << c.server_id
@ -356,7 +368,8 @@ bool Database::save_nolock() {
<< "|" << escape(c.manufacturer) << "|" << escape(c.manufacturer)
<< "|" << c.speed_ghz << "|" << c.speed_ghz
<< "|" << c.cores << "|" << c.cores
<< "|" << c.threads << "\n"; << "|" << c.threads
<< "|" << escape(c.socket_designation) << "\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()
@ -365,7 +378,8 @@ bool Database::save_nolock() {
<< "|" << escape(base_serial(c)) << "|" << escape(base_serial(c))
<< "|" << c.last_updated << "|" << c.last_updated
<< "|" << escape(c.form_factor) << "|" << escape(c.form_factor)
<< "|" << inst << "\n"; << "|" << inst
<< "|" << escape(c.socket_designation) << "\n";
} }
for (const auto& d : inv_.disks) { for (const auto& d : inv_.disks) {
f << "DISK|" << d.part_id << "|" << d.server_id f << "DISK|" << d.part_id << "|" << d.server_id
@ -394,7 +408,7 @@ bool Database::save_nolock() {
if (i) f << static_cast<char>(LS); if (i) f << static_cast<char>(LS);
f << d.vm_server_ids[i]; f << d.vm_server_ids[i];
} }
f << "\n"; f << "|" << escape(d.device_name) << "\n";
} }
for (const auto& n : inv_.network_cards) { for (const auto& n : inv_.network_cards) {
f << "NIC|" << n.part_id << "|" << n.server_id f << "NIC|" << n.part_id << "|" << n.server_id
@ -487,6 +501,7 @@ bool Database::load() {
m.manufacturer = get(7); m.manufacturer = get(7);
m.channel_config = get(8); m.channel_config = get(8);
m.other_info = get(9); m.other_info = get(9);
m.locator = get(10);
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;
@ -497,6 +512,7 @@ bool Database::load() {
m.allowed_speed_mhz = getu32(5); m.allowed_speed_mhz = getu32(5);
m.allowed_size_mb = getu64(6); m.allowed_size_mb = getu64(6);
m.installed_stick_id = get_opt_u32(7); m.installed_stick_id = get_opt_u32(7);
m.locator = get(8);
fresh.memory_slots.push_back(std::move(m)); fresh.memory_slots.push_back(std::move(m));
} else if (rec == "CPU" && flds.size() >= 10) { } else if (rec == "CPU" && flds.size() >= 10) {
CPU c; CPU c;
@ -509,6 +525,7 @@ bool Database::load() {
c.speed_ghz = flds.size() > 7 ? std::stod(flds[7]) : 0.0; c.speed_ghz = flds.size() > 7 ? std::stod(flds[7]) : 0.0;
c.cores = getu32(8); c.cores = getu32(8);
c.threads = getu32(9); c.threads = getu32(9);
c.socket_designation = get(10);
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;
@ -518,6 +535,7 @@ bool Database::load() {
c.last_updated = geti64(4); c.last_updated = geti64(4);
c.form_factor = get(5); c.form_factor = get(5);
c.installed_cpu_id = get_opt_u32(6); c.installed_cpu_id = get_opt_u32(6);
c.socket_designation = get(7);
fresh.cpu_slots.push_back(std::move(c)); fresh.cpu_slots.push_back(std::move(c));
} else if (rec == "DISK" && flds.size() >= 18) { } else if (rec == "DISK" && flds.size() >= 18) {
Disk d; Disk d;
@ -558,6 +576,7 @@ bool Database::load() {
d.partition_sizes_gb = ls_split_u64(flds.size() > 15 ? flds[15] : ""); d.partition_sizes_gb = ls_split_u64(flds.size() > 15 ? flds[15] : "");
d.vm_hostnames = uesc_split_raw(flds.size() > 16 ? flds[16] : ""); d.vm_hostnames = uesc_split_raw(flds.size() > 16 ? flds[16] : "");
d.vm_server_ids = ls_split_u32(flds.size() > 17 ? flds[17] : ""); d.vm_server_ids = ls_split_u32(flds.size() > 17 ? flds[17] : "");
d.device_name = get(18);
fresh.disks.push_back(std::move(d)); fresh.disks.push_back(std::move(d));
} else if (rec == "NIC" && flds.size() >= 13) { } else if (rec == "NIC" && flds.size() >= 13) {
NetworkCard n; NetworkCard n;
@ -793,6 +812,61 @@ uint32_t Database::upsert_part(const std::string& type_name, uint32_t server_id,
else if (type_name == PTYPE_DISK) found = try_update(inv_.disks); else if (type_name == PTYPE_DISK) found = try_update(inv_.disks);
if (found) return found; if (found) return found;
} }
// Natural key fallback when no serial — prevents duplicate inserts on each run
if (type_name == PTYPE_MEMORY_STICK || type_name == PTYPE_MEMORY_SLOT) {
auto loc_it = kv.find(K_LOCATOR);
if (loc_it != kv.end() && !loc_it->second.empty()) {
const std::string& locator = loc_it->second;
auto try_locator = [&](auto& vec, auto apply_fn) -> uint32_t {
for (auto& p : vec) {
if (p.server_id == server_id && p.locator == locator) {
apply_fn(p, kv);
p.last_updated = now;
save_nolock();
return p.part_id;
}
}
return 0u;
};
uint32_t found = 0;
if (type_name == PTYPE_MEMORY_STICK) found = try_locator(inv_.memory_sticks, apply_memory_stick);
else if (type_name == PTYPE_MEMORY_SLOT) found = try_locator(inv_.memory_slots, apply_memory_slot);
if (found) return found;
}
} else if (type_name == PTYPE_CPU || type_name == PTYPE_CPU_SLOT) {
auto sock_it = kv.find(K_SOCKET);
if (sock_it != kv.end() && !sock_it->second.empty()) {
const std::string& socket = sock_it->second;
auto try_socket = [&](auto& vec, auto apply_fn) -> uint32_t {
for (auto& p : vec) {
if (p.server_id == server_id && p.socket_designation == socket) {
apply_fn(p, kv);
p.last_updated = now;
save_nolock();
return p.part_id;
}
}
return 0u;
};
uint32_t found = 0;
if (type_name == PTYPE_CPU) found = try_socket(inv_.cpus, apply_cpu);
else if (type_name == PTYPE_CPU_SLOT) found = try_socket(inv_.cpu_slots, apply_cpu_slot);
if (found) return found;
}
} else if (type_name == PTYPE_DISK) {
auto dev_it = kv.find(K_DEVICE_NAME);
if (dev_it != kv.end() && !dev_it->second.empty()) {
const std::string& dev_name = dev_it->second;
for (auto& d : inv_.disks) {
if (d.server_id == server_id && d.device_name == dev_name) {
apply_disk(d, kv);
d.last_updated = now;
save_nolock();
return d.part_id;
}
}
}
}
} }
// Not found — insert // Not found — insert
return add_part_nolock(type_name, server_id, kv); return add_part_nolock(type_name, server_id, kv);