homelab/services/device-inventory/src/server/database.cpp
Dan V 6e1e4b4134 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>
2026-03-31 22:37:17 +02:00

989 lines
48 KiB
C++

#include "server/database.h"
#include "common/protocol.h"
#include <algorithm>
#include <ctime>
#include <fstream>
#include <iostream>
#include <sstream>
// ─────────────────────────────────────────────────────────────────────────────
// Escape / unescape: protect '|', '\n', '\' in pipe-delimited fields
// ─────────────────────────────────────────────────────────────────────────────
std::string Database::escape(const std::string& s) {
std::string out;
out.reserve(s.size() + 4);
for (char c : s) {
if (c == '\\') out += "\\\\";
else if (c == '|') out += "\\|";
else if (c == '\n') out += "\\n";
else out += c;
}
return out;
}
std::string Database::unescape(const std::string& s) {
std::string out;
out.reserve(s.size());
for (size_t i = 0; i < s.size(); ++i) {
if (s[i] == '\\' && i + 1 < s.size()) {
++i;
if (s[i] == '\\') out += '\\';
else if (s[i] == '|') out += '|';
else if (s[i] == 'n') out += '\n';
else { out += '\\'; out += s[i]; }
} else {
out += s[i];
}
}
return out;
}
// ─────────────────────────────────────────────────────────────────────────────
// split_pipe: split on '|' respecting backslash escapes
// ─────────────────────────────────────────────────────────────────────────────
static std::vector<std::string> split_pipe(const std::string& line) {
std::vector<std::string> fields;
std::string cur;
for (size_t i = 0; i < line.size(); ++i) {
if (line[i] == '\\' && i + 1 < line.size()) {
cur += line[i];
cur += line[++i];
} else if (line[i] == '|') {
fields.push_back(cur);
cur.clear();
} else {
cur += line[i];
}
}
fields.push_back(cur);
return fields;
}
// ─────────────────────────────────────────────────────────────────────────────
// List helpers (LS separator)
// ─────────────────────────────────────────────────────────────────────────────
static std::string ls_join_u64(const std::vector<uint64_t>& v) {
return join_u64(v, LS);
}
static std::string ls_join_u32(const std::vector<uint32_t>& v) {
return join_u32(v, LS);
}
static std::vector<std::string> ls_split(const std::string& s) {
if (s.empty()) return {};
return split(s, LS);
}
static std::vector<uint64_t> ls_split_u64(const std::string& s) {
std::vector<uint64_t> out;
for (const auto& p : split(s, LS))
if (!p.empty()) out.push_back(std::stoull(p));
return out;
}
static std::vector<uint32_t> ls_split_u32(const std::string& s) {
std::vector<uint32_t> out;
for (const auto& p : split(s, LS))
if (!p.empty()) out.push_back(static_cast<uint32_t>(std::stoul(p)));
return out;
}
// ─────────────────────────────────────────────────────────────────────────────
// PartBase helpers
// ─────────────────────────────────────────────────────────────────────────────
static std::string base_serial(const PartBase& p) {
return p.serial_number.has_value() ? p.serial_number.value() : "NULL";
}
static void apply_base(PartBase& p, const std::map<std::string,std::string>& kv) {
if (auto it = kv.find(K_SERIAL); it != kv.end()) {
if (it->second == "NULL") p.serial_number = std::nullopt;
else p.serial_number = it->second;
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Per-type apply helpers (static free functions)
// ─────────────────────────────────────────────────────────────────────────────
static void apply_memory_stick(MemoryStick& m, const std::map<std::string,std::string>& kv) {
apply_base(m, kv);
if (auto it = kv.find(K_SPEED_MHZ); it != kv.end()) m.speed_mhz = static_cast<uint32_t>(std::stoul(it->second));
if (auto it = kv.find(K_SIZE_MB); it != kv.end()) m.size_mb = std::stoull(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_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) {
apply_base(m, kv);
if (auto it = kv.find(K_ALLOWED_SPEED); it != kv.end()) m.allowed_speed_mhz = static_cast<uint32_t>(std::stoul(it->second));
if (auto it = kv.find(K_ALLOWED_SIZE); it != kv.end()) m.allowed_size_mb = std::stoull(it->second);
if (auto it = kv.find(K_INSTALLED_STICK); it != kv.end()) {
if (it->second == "NULL") m.installed_stick_id = std::nullopt;
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) {
apply_base(c, kv);
if (auto it = kv.find(K_NAME); it != kv.end()) c.name = it->second;
if (auto it = kv.find(K_MANUFACTURER); it != kv.end()) c.manufacturer = 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_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) {
apply_base(c, kv);
if (auto it = kv.find(K_FORM_FACTOR); it != kv.end()) c.form_factor = it->second;
if (auto it = kv.find(K_INSTALLED_CPU); it != kv.end()) {
if (it->second == "NULL") c.installed_cpu_id = std::nullopt;
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) {
apply_base(d, kv);
if (auto it = kv.find(K_MANUFACTURER); it != kv.end()) d.manufacturer = it->second;
if (auto it = kv.find(K_MODEL); it != kv.end()) d.model = it->second;
if (auto it = kv.find(K_GENERATION); it != kv.end()) d.generation = it->second;
if (auto it = kv.find(K_CONN_TYPE); it != kv.end()) d.connection_type = it->second;
if (auto it = kv.find(K_CONN_SPEED); it != kv.end()) d.connection_speed_mbps = std::stoull(it->second);
if (auto it = kv.find(K_DISK_SPEED); it != kv.end()) d.disk_speed_mbps = std::stoull(it->second);
if (auto it = kv.find(K_DISK_SIZE); it != kv.end()) d.disk_size_gb = std::stoull(it->second);
if (auto it = kv.find(K_DISK_TYPE); it != kv.end()) d.disk_type = it->second;
if (auto it = kv.find(K_AGE_YEARS); it != kv.end()) d.age_years = std::stoi(it->second);
if (auto it = kv.find(K_PARTITION_IDS); it != kv.end()) d.partition_ids = ls_split(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_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) {
apply_base(n, kv);
if (auto it = kv.find(K_MANUFACTURER); it != kv.end()) n.manufacturer = it->second;
if (auto it = kv.find(K_MODEL); it != kv.end()) n.model = it->second;
if (auto it = kv.find(K_AGE_YEARS); it != kv.end()) n.age_years = std::stoi(it->second);
if (auto it = kv.find(K_CONN_TYPE); it != kv.end()) n.connection_type = it->second;
if (auto it = kv.find(K_CONN_SPEED); it != kv.end()) n.connection_speed_mbps = std::stoull(it->second);
if (auto it = kv.find(K_MAC); it != kv.end()) n.mac_address = it->second;
if (auto it = kv.find(K_IPS); it != kv.end()) n.ip_addresses = ls_split(it->second);
if (auto it = kv.find(K_DHCP); it != kv.end()) n.dhcp = (it->second == "1" || it->second == "true");
}
// ─────────────────────────────────────────────────────────────────────────────
// Wire-row serializers
// type_name<FS>part_id<FS>server_id<FS>serial_or_NULL<FS>last_updated<FS>k1<FS>v1...
// ─────────────────────────────────────────────────────────────────────────────
static std::string base_prefix(const char* type_name, const PartBase& p) {
return std::string(type_name)
+ FS + std::to_string(p.part_id)
+ FS + std::to_string(p.server_id)
+ FS + base_serial(p)
+ FS + std::to_string(p.last_updated);
}
std::string Database::serialize_memory_stick(const MemoryStick& m) const {
return base_prefix(PTYPE_MEMORY_STICK, m)
+ FS + K_SPEED_MHZ + FS + std::to_string(m.speed_mhz)
+ FS + K_SIZE_MB + FS + std::to_string(m.size_mb)
+ FS + K_MANUFACTURER + FS + m.manufacturer
+ FS + K_CHANNEL_CONFIG + FS + m.channel_config
+ FS + K_OTHER_INFO + FS + m.other_info
+ FS + K_LOCATOR + FS + m.locator
+ "\n";
}
std::string Database::serialize_memory_slot(const MemorySlot& m) const {
std::string installed = m.installed_stick_id.has_value()
? std::to_string(m.installed_stick_id.value()) : "NULL";
return base_prefix(PTYPE_MEMORY_SLOT, m)
+ 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_INSTALLED_STICK + FS + installed
+ FS + K_LOCATOR + FS + m.locator
+ "\n";
}
std::string Database::serialize_cpu(const CPU& c) const {
return base_prefix(PTYPE_CPU, c)
+ FS + K_NAME + FS + c.name
+ FS + K_MANUFACTURER + FS + c.manufacturer
+ 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
+ "\n";
}
std::string Database::serialize_cpu_slot(const CPUSlot& c) const {
std::string installed = c.installed_cpu_id.has_value()
? std::to_string(c.installed_cpu_id.value()) : "NULL";
return base_prefix(PTYPE_CPU_SLOT, c)
+ FS + K_FORM_FACTOR + FS + c.form_factor
+ FS + K_INSTALLED_CPU + FS + installed
+ FS + K_SOCKET + FS + c.socket_designation
+ "\n";
}
std::string Database::serialize_disk(const Disk& d) const {
// Build per-list string: escape each element, join with LS
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;
// escape element (pipe/backslash could appear in partition ids or hostnames)
for (char c : v[i]) {
if (c == '\\') out += "\\\\";
else if (c == '|') out += "\\|";
else out += c;
}
}
return out;
};
return base_prefix(PTYPE_DISK, d)
+ FS + K_MANUFACTURER + FS + d.manufacturer
+ FS + K_MODEL + FS + d.model
+ FS + K_GENERATION + FS + d.generation
+ FS + K_CONN_TYPE + FS + d.connection_type
+ FS + K_CONN_SPEED + FS + std::to_string(d.connection_speed_mbps)
+ FS + K_DISK_SPEED + FS + std::to_string(d.disk_speed_mbps)
+ FS + K_DISK_SIZE + FS + std::to_string(d.disk_size_gb)
+ FS + K_DISK_TYPE + FS + d.disk_type
+ FS + K_AGE_YEARS + FS + std::to_string(d.age_years)
+ FS + K_PARTITION_IDS + FS + esc_join(d.partition_ids)
+ 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_SERVER_IDS + FS + ls_join_u32(d.vm_server_ids)
+ FS + K_DEVICE_NAME + FS + d.device_name
+ "\n";
}
std::string Database::serialize_nic(const NetworkCard& n) 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_NIC, n)
+ FS + K_MANUFACTURER + FS + n.manufacturer
+ FS + K_MODEL + FS + n.model
+ FS + K_AGE_YEARS + FS + std::to_string(n.age_years)
+ FS + K_CONN_TYPE + FS + n.connection_type
+ FS + K_CONN_SPEED + FS + std::to_string(n.connection_speed_mbps)
+ FS + K_MAC + FS + n.mac_address
+ FS + K_IPS + FS + esc_join(n.ip_addresses)
+ FS + K_DHCP + FS + (n.dhcp ? "1" : "0")
+ "\n";
}
// ─────────────────────────────────────────────────────────────────────────────
// Constructor
// ─────────────────────────────────────────────────────────────────────────────
Database::Database(std::string path) : path_(std::move(path)) {}
// ─────────────────────────────────────────────────────────────────────────────
// alloc_part_id — caller must hold mu_
// ─────────────────────────────────────────────────────────────────────────────
uint32_t Database::alloc_part_id() {
return inv_.next_part_id++;
}
// ─────────────────────────────────────────────────────────────────────────────
// save_nolock / save
// ─────────────────────────────────────────────────────────────────────────────
bool Database::save_nolock() {
std::ofstream f(path_, std::ios::trunc);
if (!f.is_open()) {
std::cerr << "[db] cannot open " << path_ << " for writing\n";
return false;
}
f << "# device-inventory database\n";
f << "META|" << inv_.next_server_id << "|"
<< inv_.next_part_type_id << "|"
<< inv_.next_part_id << "\n";
for (const auto& s : inv_.servers) {
f << "S|" << s.id
<< "|" << escape(s.name)
<< "|" << escape(s.hostname)
<< "|" << escape(s.location)
<< "|" << escape(s.description) << "\n";
}
for (const auto& pt : inv_.part_types) {
f << "PT|" << pt.id
<< "|" << escape(pt.name)
<< "|" << escape(pt.description) << "\n";
}
// Helper lambda: write LS-joined list of escaped strings as a single pipe field
auto write_str_list = [](std::ofstream& out, const std::vector<std::string>& v) {
for (size_t i = 0; i < v.size(); ++i) {
if (i) out << static_cast<char>(LS);
// escape individual elements (protect | and \ within each element)
for (char c : v[i]) {
if (c == '\\') out << "\\\\";
else if (c == '|') out << "\\|";
else out << c;
}
}
};
for (const auto& m : inv_.memory_sticks) {
f << "MS|" << m.part_id << "|" << m.server_id
<< "|" << escape(base_serial(m))
<< "|" << m.last_updated
<< "|" << m.speed_mhz
<< "|" << m.size_mb
<< "|" << escape(m.manufacturer)
<< "|" << escape(m.channel_config)
<< "|" << escape(m.other_info)
<< "|" << escape(m.locator) << "\n";
}
for (const auto& m : inv_.memory_slots) {
std::string inst = m.installed_stick_id.has_value()
? std::to_string(m.installed_stick_id.value()) : "NULL";
f << "MSLOT|" << m.part_id << "|" << m.server_id
<< "|" << escape(base_serial(m))
<< "|" << m.last_updated
<< "|" << m.allowed_speed_mhz
<< "|" << m.allowed_size_mb
<< "|" << inst
<< "|" << escape(m.locator) << "\n";
}
for (const auto& c : inv_.cpus) {
f << "CPU|" << c.part_id << "|" << c.server_id
<< "|" << escape(base_serial(c))
<< "|" << c.last_updated
<< "|" << escape(c.name)
<< "|" << escape(c.manufacturer)
<< "|" << c.speed_ghz
<< "|" << c.cores
<< "|" << c.threads
<< "|" << escape(c.socket_designation) << "\n";
}
for (const auto& c : inv_.cpu_slots) {
std::string inst = c.installed_cpu_id.has_value()
? std::to_string(c.installed_cpu_id.value()) : "NULL";
f << "CPUSLOT|" << c.part_id << "|" << c.server_id
<< "|" << escape(base_serial(c))
<< "|" << c.last_updated
<< "|" << escape(c.form_factor)
<< "|" << inst
<< "|" << escape(c.socket_designation) << "\n";
}
for (const auto& d : inv_.disks) {
f << "DISK|" << d.part_id << "|" << d.server_id
<< "|" << escape(base_serial(d))
<< "|" << d.last_updated
<< "|" << escape(d.manufacturer)
<< "|" << escape(d.model)
<< "|" << escape(d.generation)
<< "|" << escape(d.connection_type)
<< "|" << d.connection_speed_mbps
<< "|" << d.disk_speed_mbps
<< "|" << d.disk_size_gb
<< "|" << escape(d.disk_type)
<< "|" << d.age_years << "|";
write_str_list(f, d.partition_ids);
f << "|";
// partition_sizes_gb: numeric, no escape needed
for (size_t i = 0; i < d.partition_sizes_gb.size(); ++i) {
if (i) f << static_cast<char>(LS);
f << d.partition_sizes_gb[i];
}
f << "|";
write_str_list(f, d.vm_hostnames);
f << "|";
for (size_t i = 0; i < d.vm_server_ids.size(); ++i) {
if (i) f << static_cast<char>(LS);
f << d.vm_server_ids[i];
}
f << "|" << escape(d.device_name) << "\n";
}
for (const auto& n : inv_.network_cards) {
f << "NIC|" << n.part_id << "|" << n.server_id
<< "|" << escape(base_serial(n))
<< "|" << n.last_updated
<< "|" << escape(n.manufacturer)
<< "|" << escape(n.model)
<< "|" << n.age_years
<< "|" << escape(n.connection_type)
<< "|" << n.connection_speed_mbps
<< "|" << escape(n.mac_address) << "|";
write_str_list(f, n.ip_addresses);
f << "|" << (n.dhcp ? 1 : 0) << "\n";
}
return true;
}
bool Database::save() {
std::lock_guard<std::mutex> lk(mu_);
return save_nolock();
}
// ─────────────────────────────────────────────────────────────────────────────
// load
// ─────────────────────────────────────────────────────────────────────────────
bool Database::load() {
std::lock_guard<std::mutex> lk(mu_);
std::ifstream f(path_);
if (!f.is_open()) {
inv_ = Inventory{};
return true;
}
Inventory fresh;
std::string line;
while (std::getline(f, line)) {
if (line.empty() || line[0] == '#') continue;
auto flds = split_pipe(line);
if (flds.empty()) continue;
const auto& rec = flds[0];
auto get = [&](size_t i) -> std::string {
return i < flds.size() ? unescape(flds[i]) : "";
};
auto getu32 = [&](size_t i) -> uint32_t {
return i < flds.size() ? static_cast<uint32_t>(std::stoul(flds[i])) : 0;
};
auto getu64 = [&](size_t i) -> uint64_t {
return i < flds.size() ? std::stoull(flds[i]) : 0ULL;
};
auto geti64 = [&](size_t i) -> int64_t {
return i < flds.size() ? std::stoll(flds[i]) : 0LL;
};
// serial field: raw flds[i] (not unescaped via get) but compare to "NULL"
auto get_serial = [&](size_t i) -> std::optional<std::string> {
if (i >= flds.size() || flds[i] == "NULL") return std::nullopt;
return unescape(flds[i]);
};
auto get_opt_u32 = [&](size_t i) -> std::optional<uint32_t> {
if (i >= flds.size() || flds[i] == "NULL") return std::nullopt;
return static_cast<uint32_t>(std::stoul(flds[i]));
};
if (rec == "META" && flds.size() >= 4) {
fresh.next_server_id = getu32(1);
fresh.next_part_type_id = getu32(2);
fresh.next_part_id = getu32(3);
} else if (rec == "S" && flds.size() >= 6) {
Server s;
s.id = getu32(1);
s.name = get(2);
s.hostname = get(3);
s.location = get(4);
s.description = get(5);
fresh.servers.push_back(std::move(s));
} else if (rec == "PT" && flds.size() >= 4) {
PartType pt;
pt.id = getu32(1);
pt.name = get(2);
pt.description = get(3);
fresh.part_types.push_back(std::move(pt));
} else if (rec == "MS" && flds.size() >= 10) {
MemoryStick m;
m.part_id = getu32(1);
m.server_id = getu32(2);
m.serial_number = get_serial(3);
m.last_updated = geti64(4);
m.speed_mhz = getu32(5);
m.size_mb = getu64(6);
m.manufacturer = get(7);
m.channel_config = get(8);
m.other_info = get(9);
m.locator = get(10);
fresh.memory_sticks.push_back(std::move(m));
} else if (rec == "MSLOT" && flds.size() >= 8) {
MemorySlot m;
m.part_id = getu32(1);
m.server_id = getu32(2);
m.serial_number = get_serial(3);
m.last_updated = geti64(4);
m.allowed_speed_mhz = getu32(5);
m.allowed_size_mb = getu64(6);
m.installed_stick_id = get_opt_u32(7);
m.locator = get(8);
fresh.memory_slots.push_back(std::move(m));
} else if (rec == "CPU" && flds.size() >= 10) {
CPU c;
c.part_id = getu32(1);
c.server_id = getu32(2);
c.serial_number = get_serial(3);
c.last_updated = geti64(4);
c.name = get(5);
c.manufacturer = get(6);
c.speed_ghz = flds.size() > 7 ? std::stod(flds[7]) : 0.0;
c.cores = getu32(8);
c.threads = getu32(9);
c.socket_designation = get(10);
fresh.cpus.push_back(std::move(c));
} else if (rec == "CPUSLOT" && flds.size() >= 7) {
CPUSlot c;
c.part_id = getu32(1);
c.server_id = getu32(2);
c.serial_number = get_serial(3);
c.last_updated = geti64(4);
c.form_factor = get(5);
c.installed_cpu_id = get_opt_u32(6);
c.socket_designation = get(7);
fresh.cpu_slots.push_back(std::move(c));
} else if (rec == "DISK" && flds.size() >= 18) {
Disk d;
d.part_id = getu32(1);
d.server_id = getu32(2);
d.serial_number = get_serial(3);
d.last_updated = geti64(4);
d.manufacturer = get(5);
d.model = get(6);
d.generation = get(7);
d.connection_type = get(8);
d.connection_speed_mbps = getu64(9);
d.disk_speed_mbps = getu64(10);
d.disk_size_gb = getu64(11);
d.disk_type = get(12);
d.age_years = flds.size() > 13 ? std::stoi(flds[13]) : -1;
// Note: flds[14..17] have NOT been through unescape() yet (we used raw flds)
// Re-split using the raw (escaped) field and unescape each element
auto uesc_split_raw = [](const std::string& raw) -> std::vector<std::string> {
if (raw.empty()) return {};
std::vector<std::string> out;
for (const auto& elem : split(raw, LS)) {
std::string u;
for (size_t i = 0; i < elem.size(); ++i) {
if (elem[i] == '\\' && i+1 < elem.size()) {
++i;
if (elem[i] == '\\') u += '\\';
else if (elem[i] == '|') u += '|';
else if (elem[i] == 'n') u += '\n';
else { u += '\\'; u += elem[i]; }
} else u += elem[i];
}
out.push_back(u);
}
return out;
};
d.partition_ids = uesc_split_raw(flds.size() > 14 ? flds[14] : "");
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_server_ids = ls_split_u32(flds.size() > 17 ? flds[17] : "");
d.device_name = get(18);
fresh.disks.push_back(std::move(d));
} else if (rec == "NIC" && flds.size() >= 13) {
NetworkCard n;
n.part_id = getu32(1);
n.server_id = getu32(2);
n.serial_number = get_serial(3);
n.last_updated = geti64(4);
n.manufacturer = get(5);
n.model = get(6);
n.age_years = flds.size() > 7 ? std::stoi(flds[7]) : -1;
n.connection_type = get(8);
n.connection_speed_mbps= getu64(9);
n.mac_address = get(10);
// ip_addresses: LS separated in flds[11]
{
auto& raw = flds[11];
for (auto& p : split(raw, LS))
n.ip_addresses.push_back(unescape(p));
}
n.dhcp = (flds.size() > 12 && flds[12] == "1");
fresh.network_cards.push_back(std::move(n));
}
}
inv_ = std::move(fresh);
return true;
}
// ─────────────────────────────────────────────────────────────────────────────
// Servers
// ─────────────────────────────────────────────────────────────────────────────
Server Database::add_server(std::string name, std::string hostname,
std::string location, std::string description) {
std::lock_guard<std::mutex> lk(mu_);
Server s;
s.id = inv_.next_server_id++;
s.name = std::move(name);
s.hostname = std::move(hostname);
s.location = std::move(location);
s.description = std::move(description);
inv_.servers.push_back(s);
save_nolock();
return s;
}
std::vector<Server> Database::list_servers() const {
std::lock_guard<std::mutex> lk(mu_);
return inv_.servers;
}
std::optional<Server> Database::get_server(uint32_t id) const {
std::lock_guard<std::mutex> lk(mu_);
for (const auto& s : inv_.servers)
if (s.id == id) return s;
return std::nullopt;
}
bool Database::edit_server(uint32_t id, const std::string& field, const std::string& value) {
std::lock_guard<std::mutex> lk(mu_);
for (auto& s : inv_.servers) {
if (s.id != id) continue;
if (field == "name") s.name = value;
else if (field == "hostname") s.hostname = value;
else if (field == "location") s.location = value;
else if (field == "description") s.description = value;
else return false;
save_nolock();
return true;
}
return false;
}
bool Database::remove_server(uint32_t id) {
std::lock_guard<std::mutex> lk(mu_);
auto& v = inv_.servers;
auto it = std::find_if(v.begin(), v.end(), [id](const Server& s){ return s.id == id; });
if (it == v.end()) return false;
v.erase(it);
save_nolock();
return true;
}
// ─────────────────────────────────────────────────────────────────────────────
// Part types
// ─────────────────────────────────────────────────────────────────────────────
PartType Database::add_part_type(std::string name, std::string description) {
std::lock_guard<std::mutex> lk(mu_);
PartType pt;
pt.id = inv_.next_part_type_id++;
pt.name = std::move(name);
pt.description = std::move(description);
inv_.part_types.push_back(pt);
save_nolock();
return pt;
}
std::vector<PartType> Database::list_part_types() const {
std::lock_guard<std::mutex> lk(mu_);
return inv_.part_types;
}
std::optional<PartType> Database::get_part_type(uint32_t id) const {
std::lock_guard<std::mutex> lk(mu_);
for (const auto& pt : inv_.part_types)
if (pt.id == id) return pt;
return std::nullopt;
}
bool Database::edit_part_type(uint32_t id, const std::string& field, const std::string& value) {
std::lock_guard<std::mutex> lk(mu_);
for (auto& pt : inv_.part_types) {
if (pt.id != id) continue;
if (field == "name") pt.name = value;
else if (field == "description") pt.description = value;
else return false;
save_nolock();
return true;
}
return false;
}
bool Database::remove_part_type(uint32_t id) {
std::lock_guard<std::mutex> lk(mu_);
auto& v = inv_.part_types;
auto it = std::find_if(v.begin(), v.end(), [id](const PartType& pt){ return pt.id == id; });
if (it == v.end()) return false;
v.erase(it);
save_nolock();
return true;
}
// ─────────────────────────────────────────────────────────────────────────────
// add_part_nolock — caller must hold mu_
// ─────────────────────────────────────────────────────────────────────────────
uint32_t Database::add_part_nolock(const std::string& type_name, uint32_t server_id,
const std::map<std::string,std::string>& kv) {
uint32_t pid = alloc_part_id();
int64_t now = static_cast<int64_t>(time(nullptr));
if (type_name == PTYPE_MEMORY_STICK) {
MemoryStick m; m.part_id = pid; m.server_id = server_id; m.last_updated = now;
apply_memory_stick(m, kv);
inv_.memory_sticks.push_back(std::move(m));
} else if (type_name == PTYPE_MEMORY_SLOT) {
MemorySlot m; m.part_id = pid; m.server_id = server_id; m.last_updated = now;
apply_memory_slot(m, kv);
inv_.memory_slots.push_back(std::move(m));
} else if (type_name == PTYPE_CPU) {
CPU c; c.part_id = pid; c.server_id = server_id; c.last_updated = now;
apply_cpu(c, kv);
inv_.cpus.push_back(std::move(c));
} else if (type_name == PTYPE_CPU_SLOT) {
CPUSlot c; c.part_id = pid; c.server_id = server_id; c.last_updated = now;
apply_cpu_slot(c, kv);
inv_.cpu_slots.push_back(std::move(c));
} else if (type_name == PTYPE_DISK) {
Disk d; d.part_id = pid; d.server_id = server_id; d.last_updated = now;
apply_disk(d, kv);
inv_.disks.push_back(std::move(d));
} else if (type_name == PTYPE_NIC) {
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 {
// Unknown type — no-op, reclaim the id by decrementing
--inv_.next_part_id;
return 0;
}
save_nolock();
return pid;
}
// ─────────────────────────────────────────────────────────────────────────────
// add_part (public, locks)
// ─────────────────────────────────────────────────────────────────────────────
uint32_t Database::add_part(const std::string& type_name, uint32_t server_id,
const std::map<std::string,std::string>& kv) {
std::lock_guard<std::mutex> lk(mu_);
return add_part_nolock(type_name, server_id, kv);
}
// ─────────────────────────────────────────────────────────────────────────────
// upsert_part
// ─────────────────────────────────────────────────────────────────────────────
uint32_t Database::upsert_part(const std::string& type_name, uint32_t server_id,
const std::map<std::string,std::string>& kv) {
std::lock_guard<std::mutex> lk(mu_);
int64_t now = static_cast<int64_t>(time(nullptr));
if (type_name == PTYPE_NIC) {
auto mac_it = kv.find(K_MAC);
if (mac_it != kv.end() && !mac_it->second.empty()) {
for (auto& n : inv_.network_cards) {
if (n.server_id == server_id && n.mac_address == mac_it->second) {
apply_nic(n, kv);
n.last_updated = now;
save_nolock();
return n.part_id;
}
}
}
} else {
auto ser_it = kv.find(K_SERIAL);
if (ser_it != kv.end() && ser_it->second != "NULL" && !ser_it->second.empty()) {
const std::string& serial = ser_it->second;
// Search in the collection matching type_name
auto try_update = [&](auto& vec) -> uint32_t {
for (auto& p : vec) {
if (p.server_id == server_id && p.serial_number.has_value()
&& p.serial_number.value() == serial) {
if constexpr (std::is_same_v<std::decay_t<decltype(p)>, MemoryStick>)
apply_memory_stick(p, kv);
else if constexpr (std::is_same_v<std::decay_t<decltype(p)>, MemorySlot>)
apply_memory_slot(p, kv);
else if constexpr (std::is_same_v<std::decay_t<decltype(p)>, CPU>)
apply_cpu(p, kv);
else if constexpr (std::is_same_v<std::decay_t<decltype(p)>, CPUSlot>)
apply_cpu_slot(p, kv);
else if constexpr (std::is_same_v<std::decay_t<decltype(p)>, Disk>)
apply_disk(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_update(inv_.memory_sticks);
else if (type_name == PTYPE_MEMORY_SLOT) found = try_update(inv_.memory_slots);
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);
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
return add_part_nolock(type_name, server_id, kv);
}
// ─────────────────────────────────────────────────────────────────────────────
// edit_part
// ─────────────────────────────────────────────────────────────────────────────
bool Database::edit_part(uint32_t part_id, const std::map<std::string,std::string>& kv) {
std::lock_guard<std::mutex> lk(mu_);
int64_t now = static_cast<int64_t>(time(nullptr));
auto try_edit = [&](auto& vec, auto apply_fn) -> bool {
for (auto& p : vec) {
if (p.part_id != part_id) continue;
apply_fn(p, kv);
p.last_updated = now;
save_nolock();
return true;
}
return false;
};
if (try_edit(inv_.memory_sticks, apply_memory_stick)) return true;
if (try_edit(inv_.memory_slots, apply_memory_slot)) return true;
if (try_edit(inv_.cpus, apply_cpu)) 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_.network_cards, apply_nic)) return true;
return false;
}
// ─────────────────────────────────────────────────────────────────────────────
// remove_part
// ─────────────────────────────────────────────────────────────────────────────
bool Database::remove_part(uint32_t part_id) {
std::lock_guard<std::mutex> lk(mu_);
auto try_remove = [&](auto& vec) -> bool {
auto it = std::find_if(vec.begin(), vec.end(),
[part_id](const auto& p){ return p.part_id == part_id; });
if (it == vec.end()) return false;
vec.erase(it);
return true;
};
bool removed = try_remove(inv_.memory_sticks)
|| try_remove(inv_.memory_slots)
|| try_remove(inv_.cpus)
|| try_remove(inv_.cpu_slots)
|| try_remove(inv_.disks)
|| try_remove(inv_.network_cards);
if (removed) save_nolock();
return removed;
}
// ─────────────────────────────────────────────────────────────────────────────
// list_parts / get_part_row
// ─────────────────────────────────────────────────────────────────────────────
std::vector<std::string> Database::list_parts(uint32_t server_id,
const std::string& type_filter) const {
std::lock_guard<std::mutex> lk(mu_);
std::vector<std::string> rows;
bool all = type_filter.empty();
if (all || type_filter == PTYPE_MEMORY_STICK)
for (const auto& m : inv_.memory_sticks)
if (m.server_id == server_id) rows.push_back(serialize_memory_stick(m));
if (all || type_filter == PTYPE_MEMORY_SLOT)
for (const auto& m : inv_.memory_slots)
if (m.server_id == server_id) rows.push_back(serialize_memory_slot(m));
if (all || type_filter == PTYPE_CPU)
for (const auto& c : inv_.cpus)
if (c.server_id == server_id) rows.push_back(serialize_cpu(c));
if (all || type_filter == PTYPE_CPU_SLOT)
for (const auto& c : inv_.cpu_slots)
if (c.server_id == server_id) rows.push_back(serialize_cpu_slot(c));
if (all || type_filter == PTYPE_DISK)
for (const auto& d : inv_.disks)
if (d.server_id == server_id) rows.push_back(serialize_disk(d));
if (all || type_filter == PTYPE_NIC)
for (const auto& n : inv_.network_cards)
if (n.server_id == server_id) rows.push_back(serialize_nic(n));
return rows;
}
std::string Database::get_part_row(uint32_t part_id) const {
std::lock_guard<std::mutex> lk(mu_);
for (const auto& m : inv_.memory_sticks)
if (m.part_id == part_id) return serialize_memory_stick(m);
for (const auto& m : inv_.memory_slots)
if (m.part_id == part_id) return serialize_memory_slot(m);
for (const auto& c : inv_.cpus)
if (c.part_id == part_id) return serialize_cpu(c);
for (const auto& c : inv_.cpu_slots)
if (c.part_id == part_id) return serialize_cpu_slot(c);
for (const auto& d : inv_.disks)
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);
return "";
}
// ─────────────────────────────────────────────────────────────────────────────
// get_nics_for_server
// ─────────────────────────────────────────────────────────────────────────────
std::vector<NetworkCard> Database::get_nics_for_server(uint32_t server_id) const {
std::lock_guard<std::mutex> lk(mu_);
std::vector<NetworkCard> result;
for (const auto& n : inv_.network_cards)
if (n.server_id == server_id) result.push_back(n);
return result;
}