diff --git a/CMakeLists.txt b/CMakeLists.txt index c0d517c5e4..b55a26438d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1068,6 +1068,8 @@ set(LIBNETDATA_FILES src/libnetdata/os/mmap_limit.h src/libnetdata/signals/signals.c src/libnetdata/signals/signals.h + src/libnetdata/os/machine_id.c + src/libnetdata/os/machine_id.h ) list(APPEND LIBNETDATA_FILES ${INICFG_FILES}) diff --git a/src/daemon/daemon-shutdown.c b/src/daemon/daemon-shutdown.c index bd44e71545..7e75a1f1a8 100644 --- a/src/daemon/daemon-shutdown.c +++ b/src/daemon/daemon-shutdown.c @@ -300,13 +300,16 @@ void netdata_cleanup_and_exit(EXIT_REASON reason, const char *action, const char sqlite_close_databases(); watcher_step_complete(WATCHER_STEP_ID_CLOSE_SQL_DATABASES); sqlite_library_shutdown(); - - + // unlink the pid - if(pidfile && *pidfile) { - if(unlink(pidfile) != 0) - netdata_log_error("EXIT: cannot unlink pidfile '%s'.", pidfile); - } + if(pidfile && *pidfile && unlink(pidfile) != 0) + netdata_log_error("EXIT: cannot unlink pidfile '%s'.", pidfile); + + // unlink the pipe + const char *pipe = daemon_pipename(); + if(pipe && *pipe && unlink(pipe) != 0) + netdata_log_error("EXIT: cannot unlink netdatacli socket file '%s'.", pipe); + watcher_step_complete(WATCHER_STEP_ID_REMOVE_PID_FILE); netdata_ssl_cleanup(); diff --git a/src/daemon/daemon-status-file.c b/src/daemon/daemon-status-file.c index f7a54f7845..c19be8053f 100644 --- a/src/daemon/daemon-status-file.c +++ b/src/daemon/daemon-status-file.c @@ -9,7 +9,7 @@ #include <openssl/pem.h> #include <openssl/err.h> -#define STATUS_FILE_VERSION 9 +#define STATUS_FILE_VERSION 10 #define STATUS_FILENAME "status-netdata.json" @@ -129,6 +129,7 @@ static void daemon_status_file_to_json(BUFFER *wb, DAEMON_STATUS_FILE *ds) { buffer_json_member_add_object(wb, "host"); // ECS { + buffer_json_member_add_uuid_compact(wb, "id", ds->machine_id.uuid); buffer_json_member_add_string_or_empty(wb, "architecture", ds->architecture); // ECS buffer_json_member_add_string_or_empty(wb, "virtualization", ds->virtualization); // custom buffer_json_member_add_string_or_empty(wb, "container", ds->container); // custom @@ -136,7 +137,7 @@ static void daemon_status_file_to_json(BUFFER *wb, DAEMON_STATUS_FILE *ds) { buffer_json_member_add_object(wb, "boot"); // ECS { - buffer_json_member_add_uuid(wb, "id", ds->boot_id.uuid); // ECS + buffer_json_member_add_uuid_compact(wb, "id", ds->boot_id.uuid); // ECS } buffer_json_object_close(wb); @@ -224,6 +225,7 @@ static bool daemon_status_file_from_json(json_object *jobj, void *data, BUFFER * bool required_v3 = version >= 3 ? strict : false; bool required_v4 = version >= 4 ? strict : false; bool required_v5 = version >= 5 ? strict : false; + bool required_v10 = version >= 10 ? strict : false; // Parse timestamp JSONC_PARSE_TXT2CHAR_OR_ERROR_AND_RETURN(jobj, path, "@timestamp", datetime, error, required_v1); @@ -254,6 +256,7 @@ static bool daemon_status_file_from_json(json_object *jobj, void *data, BUFFER * // Parse host object JSONC_PARSE_SUBOBJECT(jobj, path, "host", error, required_v1, { + JSONC_PARSE_TXT2UUID_OR_ERROR_AND_RETURN(jobj, path, "id", ds->machine_id.uuid, error, required_v10); JSONC_PARSE_TXT2CHAR_OR_ERROR_AND_RETURN(jobj, path, "architecture", ds->architecture, error, required_v1); JSONC_PARSE_TXT2CHAR_OR_ERROR_AND_RETURN(jobj, path, "virtualization", ds->virtualization, error, required_v1); JSONC_PARSE_TXT2CHAR_OR_ERROR_AND_RETURN(jobj, path, "container", ds->container, error, required_v1); @@ -399,6 +402,9 @@ static void daemon_status_file_refresh(DAEMON_STATUS status) { session_status.host_id = UUID_ZERO; } + if(UUIDiszero(session_status.machine_id)) + session_status.machine_id = os_machine_id(); + // copy items from the old status if they are not set if(UUIDiszero(session_status.claim_id)) session_status.claim_id = last_session_status.claim_id; diff --git a/src/daemon/daemon-status-file.h b/src/daemon/daemon-status-file.h index d2b4261267..c9f6133510 100644 --- a/src/daemon/daemon-status-file.h +++ b/src/daemon/daemon-status-file.h @@ -44,6 +44,7 @@ typedef struct daemon_status_file { ND_UUID host_id; // the machine guid of the agent ND_UUID node_id; // the Netdata Cloud node id of the agent ND_UUID claim_id; // the Netdata Cloud claim id of the agent + ND_UUID machine_id; // the unique machine id of the system struct { usec_t init_started_ut; diff --git a/src/daemon/main.c b/src/daemon/main.c index 7e371d5798..8e3c2051b7 100644 --- a/src/daemon/main.c +++ b/src/daemon/main.c @@ -902,6 +902,7 @@ int netdata_main(int argc, char **argv) { delta_startup_time("web server sockets"); if(web_server_mode != WEB_SERVER_MODE_NONE) { + errno_clear(); if (!api_listen_sockets_setup()) { exit_initiated_add(EXIT_REASON_ALREADY_RUNNING); daemon_status_file_update_status(DAEMON_STATUS_NONE); diff --git a/src/libnetdata/os/machine_id.c b/src/libnetdata/os/machine_id.c new file mode 100644 index 0000000000..ca2ecabfd1 --- /dev/null +++ b/src/libnetdata/os/machine_id.c @@ -0,0 +1,186 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "machine_id.h" +#include "libnetdata/libnetdata.h" + +static ND_UUID cached_machine_id = { 0 }; +static SPINLOCK spinlock = SPINLOCK_INITIALIZER; + +#if defined(OS_LINUX) + +static ND_UUID get_machine_id(void) { + ND_UUID machine_id = { 0 }; + + // Try systemd machine-id locations first + const char *locations[] = { + "/etc/machine-id", // systemd standard location + "/var/lib/dbus/machine-id", // fallback for older systems or different distros + "/sys/class/dmi/id/product_uuid" // hardware-based UUID from DMI + }; + + for (size_t i = 0; i < _countof(locations); i++) { + char filename[FILENAME_MAX + 1]; + snprintfz(filename, sizeof(filename), "%s%s", + netdata_configured_host_prefix ? netdata_configured_host_prefix : "", locations[i]); + + char buf[128]; + if (read_txt_file(filename, buf, sizeof(buf)) == 0) { + if (uuid_parse(trim(buf), machine_id.uuid) == 0) + return machine_id; + } + } + + // If no reliable machine ID could be found, return NO_MACHINE_ID + return NO_MACHINE_ID; +} + +#elif defined(OS_FREEBSD) + +static ND_UUID get_machine_id(void) { + ND_UUID machine_id = { 0 }; + char buf[128]; + + // Try FreeBSD host ID first + char filename[FILENAME_MAX + 1]; + snprintfz(filename, sizeof(filename), "%s/etc/hostid", + netdata_configured_host_prefix ? netdata_configured_host_prefix : ""); + + if (read_txt_file(filename, buf, sizeof(buf)) == 0) { + if (uuid_parse(trim(buf), machine_id.uuid) == 0) + return machine_id; + } + + // Fallback: Read system kern.hostuuid sysctl + size_t len = sizeof(buf); + if (sysctlbyname("kern.hostuuid", buf, &len, NULL, 0) == 0) { + if (uuid_parse(trim(buf), machine_id.uuid) == 0) + return machine_id; + } + + // If no reliable machine ID could be found, return NO_MACHINE_ID + return NO_MACHINE_ID; +} + +#elif defined(OS_MACOS) + +#include <IOKit/IOKitLib.h> + +static ND_UUID get_machine_id(void) { + ND_UUID machine_id = { 0 }; + + // First try to get the platform UUID + io_registry_entry_t ioRegistryRoot = IORegistryEntryFromPath(kIOMasterPortDefault, "IOService:/"); + if (ioRegistryRoot) { + CFStringRef uuidCf = (CFStringRef) IORegistryEntryCreateCFProperty( + ioRegistryRoot, + CFSTR(kIOPlatformUUIDKey), + kCFAllocatorDefault, + 0); + + if (uuidCf) { + char uuid_str[UUID_STR_LEN]; + if (CFStringGetCString(uuidCf, uuid_str, sizeof(uuid_str), kCFStringEncodingUTF8)) { + if (uuid_parse(uuid_str, machine_id.uuid) == 0) { + CFRelease(uuidCf); + IOObjectRelease(ioRegistryRoot); + return machine_id; + } + } + CFRelease(uuidCf); + } + IOObjectRelease(ioRegistryRoot); + } + + // Fallback to IOPlatformExpertDevice's IOPlatformSerialNumber + io_service_t platformExpert = IOServiceGetMatchingService( + kIOMasterPortDefault, + IOServiceMatching("IOPlatformExpertDevice")); + + if (platformExpert) { + CFStringRef serialNumberCf = (CFStringRef) IORegistryEntryCreateCFProperty( + platformExpert, + CFSTR(kIOPlatformSerialNumberKey), + kCFAllocatorDefault, + 0); + + if (serialNumberCf) { + char serial_str[128]; + if (CFStringGetCString(serialNumberCf, serial_str, sizeof(serial_str), kCFStringEncodingUTF8)) { + // Hardware serial numbers are considered reliable and stable + if (strlen(serial_str) > 0) { + // Generate a UUID from the serial number + char uuid_input[150]; + snprintfz(uuid_input, sizeof(uuid_input), "mac-serial:%s", serial_str); + machine_id = UUID_generate_from_hash(uuid_input, strlen(uuid_input)); + + CFRelease(serialNumberCf); + IOObjectRelease(platformExpert); + return machine_id; + } + } + CFRelease(serialNumberCf); + } + IOObjectRelease(platformExpert); + } + + // If no reliable machine ID could be found, return NO_MACHINE_ID + return NO_MACHINE_ID; +} + +#elif defined(OS_WINDOWS) +#include <windows.h> + +static ND_UUID get_machine_id(void) { + ND_UUID machine_id = { 0 }; + HKEY hKey; + + // Try to get MachineGuid from registry - this is the most reliable machine ID on Windows + if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, L"SOFTWARE\\Microsoft\\Cryptography", 0, KEY_READ, &hKey) == ERROR_SUCCESS) { + WCHAR guidW[64]; + DWORD guidSize = sizeof(guidW); + DWORD type = REG_SZ; + + if (RegQueryValueExW(hKey, L"MachineGuid", NULL, &type, (LPBYTE)guidW, &guidSize) == ERROR_SUCCESS) { + char guid_str[UUID_STR_LEN]; + + // Convert GUID to UTF-8 + if (WideCharToMultiByte(CP_UTF8, 0, guidW, -1, guid_str, sizeof(guid_str), NULL, NULL) > 0) { + if (uuid_parse(guid_str, machine_id.uuid) == 0) { + RegCloseKey(hKey); + return machine_id; + } + } + } + RegCloseKey(hKey); + } + + // If no reliable machine ID could be found, return NO_MACHINE_ID + return NO_MACHINE_ID; +} + +#endif // OS_WINDOWS + +ND_UUID os_machine_id(void) { + // Fast path - return cached value if available + if(!UUIDiszero(cached_machine_id)) + return cached_machine_id; + + spinlock_lock(&spinlock); + + // Check again under lock in case another thread set it + if(UUIDiszero(cached_machine_id)) { + cached_machine_id = get_machine_id(); + + // Log the result if debugging is enabled + if(UUIDeq(cached_machine_id, NO_MACHINE_ID)) + nd_log(NDLS_DAEMON, NDLP_WARNING, "OS_MACHINE_ID: Could not detect a reliable machine ID"); + else { + char buf[UUID_STR_LEN]; + uuid_unparse_lower(cached_machine_id.uuid, buf); + nd_log(NDLS_DAEMON, NDLP_WARNING, "OS_MACHINE_ID: machine ID found '%s'", buf); + } + } + + spinlock_unlock(&spinlock); + return cached_machine_id; +} diff --git a/src/libnetdata/os/machine_id.h b/src/libnetdata/os/machine_id.h new file mode 100644 index 0000000000..a7f92421ce --- /dev/null +++ b/src/libnetdata/os/machine_id.h @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef NETDATA_MACHINE_ID_H +#define NETDATA_MACHINE_ID_H + +#include "libnetdata/libnetdata.h" +#include "libnetdata/uuid/uuid.h" + +/** + * Special value returned when a reliable machine ID cannot be determined + */ +#define NO_MACHINE_ID (ND_UUID){ .parts = { .hig64 = 1, .low64 = 1 } } + +/** + * Get the machine ID + * + * Returns a UUID that uniquely identifies the machine. + * On Linux, this is the systemd machine-id. + * On other systems, it uses platform-specific values. + * + * The value is cached after first call. + * Returns NO_MACHINE_ID when a reliable machine ID cannot be detected. + * + * @return ND_UUID The machine ID + */ +ND_UUID os_machine_id(void); + +#endif // NETDATA_MACHINE_ID_H diff --git a/src/libnetdata/os/os.h b/src/libnetdata/os/os.h index 19dbe4caf7..c9b76fc3ae 100644 --- a/src/libnetdata/os/os.h +++ b/src/libnetdata/os/os.h @@ -40,6 +40,7 @@ #include "run_dir.h" #include "file_lock.h" #include "mmap_limit.h" +#include "machine_id.h" // this includes windows.h to the whole of netdata // so various conflicts arise