public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
* [PATCH manager/proxmox-mini-journalreader v2 0/2] report: list recent reboot events and their kernel version
@ 2026-08-05  9:45 Maximiliano Sandoval
  2026-08-05  9:45 ` [PATCH proxmox-mini-journalreader v2 1/2] add -B parameter to lists boots Maximiliano Sandoval
  2026-08-05  9:45 ` [PATCH manager v2 2/2] report: list recent reboot events and their kernel version Maximiliano Sandoval
  0 siblings, 2 replies; 3+ messages in thread
From: Maximiliano Sandoval @ 2026-08-05  9:45 UTC (permalink / raw)
  To: pve-devel

See commits.


proxmox-mini-journalreader:

Maximiliano Sandoval (1):
  add -B parameter to lists boots

 debian/control           |   2 +-
 src/Makefile             |   2 +-
 src/mini-journalreader.c | 154 ++++++++++++++++++++++++++++++++++++++-
 3 files changed, 155 insertions(+), 3 deletions(-)


pve-manager:

Maximiliano Sandoval (1):
  report: list recent reboot events and their kernel version

 PVE/Report.pm | 1 +
 1 file changed, 1 insertion(+)


Summary over all repositories:
  4 files changed, 156 insertions(+), 3 deletions(-)

-- 
Generated by murpp 0.12.1




^ permalink raw reply	[flat|nested] 3+ messages in thread

* [PATCH proxmox-mini-journalreader v2 1/2] add -B parameter to lists boots
  2026-08-05  9:45 [PATCH manager/proxmox-mini-journalreader v2 0/2] report: list recent reboot events and their kernel version Maximiliano Sandoval
@ 2026-08-05  9:45 ` Maximiliano Sandoval
  2026-08-05  9:45 ` [PATCH manager v2 2/2] report: list recent reboot events and their kernel version Maximiliano Sandoval
  1 sibling, 0 replies; 3+ messages in thread
From: Maximiliano Sandoval @ 2026-08-05  9:45 UTC (permalink / raw)
  To: pve-devel

The parameter will print all boots, in practice it should be piped into tail.

Sample output:

2026-07-27 08:28	2026-07-27 15:56	3e3813556d53477f9a139a7cb2f3e8f2	7.0.14-6-pve
2026-07-27 15:57	2026-07-27 17:03	5a0324e1ac3f4f24907a21ab81b67384	7.0.14-7-pve
2026-07-28 08:26	2026-07-28 17:07	e62bc71e85274f21a1bfb954be38c071	7.0.14-7-pve
2026-07-29 08:43	2026-07-29 08:48	7f668e4499644ea1b8339d6b8c29d53d	7.0.14-7-pve
2026-07-29 08:49	2026-07-29 17:09	9ff5eb6f0485466383e0b89512f937c9	7.0.14-8-pve

glib is added as a dependency for simplicity of memory management.

As per SD_JOURNAL_QUERY_UNIQUE(3), sd_journal_query_unique ignore matches hence
why we need to iterate twice:

     Note that these functions currently are not influenced by matches
     set with sd_journal_add_match() but this might change in a later
     version of this software.

Signed-off-by: Maximiliano Sandoval <m.sandoval@proxmox.com>
---

Notes:
    We usually require the output from `last` or similar in support to query the
    previously booted kernel versions. We add this helper to avoid adding a
    dependency on either last or wtmpdb on the ISO images.
    
    This seems quick enough for ~400 boots:
    
    time sudo ./proxmox-mini-journalreader -B | wc -l
    417
    
    real	0m0,143s
    user	0m0,004s
    sys	0m0,004s
    
    Open questions:
    
    - Should it list the latest boots first? It amounts to reversing the sign on
    boot_info_cmp.
    - We assume the first line in the journal is the "Linux version" line. This is
    true as of now in practice. One could add a match like:
    
       sd_journal_add_match(j, "_TRANSPORT=kernel", strlen("_TRANSPORT=kernel"));
    
      if this is undesirable.
    
    Differences from v1:
     - Replace uint with size_t
     - Remove the use of a second journal
     - Copy only 32 chars instead of 33. We were copying \0 which is added by
       g_strndup anyways and 32 is the actual length of a BOOT_ID
     - use strlen("BOOT_ID=") instead of sizeof("BOOT_ID") so it does not work by accident
     - Use proper signature for GCompareFunc
     - Add a couple of guards for empty messages

 debian/control           |   2 +-
 src/Makefile             |   2 +-
 src/mini-journalreader.c | 154 ++++++++++++++++++++++++++++++++++++++-
 3 files changed, 155 insertions(+), 3 deletions(-)

diff --git a/debian/control b/debian/control
index 8280f6d..d7d8cc0 100644
--- a/debian/control
+++ b/debian/control
@@ -2,7 +2,7 @@ Source: proxmox-mini-journalreader
 Section: admin
 Priority: optional
 Maintainer: Proxmox Support Team <support@proxmox.com>
-Build-Depends: debhelper-compat (= 13), libsystemd-dev, pkg-config, scdoc,
+Build-Depends: debhelper-compat (= 13), libsystemd-dev, pkg-config, scdoc, libglib2.0-dev,
 Standards-Version: 4.6.2
 
 Package: proxmox-mini-journalreader
diff --git a/src/Makefile b/src/Makefile
index e64e066..c89e74a 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -5,7 +5,7 @@ LIBEXEC_DIR ?= $(DESTDIR)/usr/libexec/
 MAN1_DIR ?= $(DESTDIR)/usr/share/man/man1
 MANPAGE ?= $(PROGRAM).1
 
-LIBS := libsystemd
+LIBS := libsystemd glib-2.0
 CFLAGS += -Wall -Wextra -Wl,-z,relro -g -O2 --std=gnu11
 CFLAGS += -fstack-protector-strong -D_FORTIFY_SOURCE=2
 CFLAGS += $(shell pkg-config --cflags $(LIBS))
diff --git a/src/mini-journalreader.c b/src/mini-journalreader.c
index fc78f65..7d0d76b 100644
--- a/src/mini-journalreader.c
+++ b/src/mini-journalreader.c
@@ -21,6 +21,7 @@
 
 #include <errno.h>
 #include <fnmatch.h>
+#include <glib.h>
 #include <stdbool.h>
 #include <stdint.h>
 #include <stdio.h>
@@ -37,6 +38,148 @@ bool json = false;
 bool structured = false;
 bool first_line = true;
 
+typedef struct {
+    char *boot_id;
+    char *version;
+    uint64_t first_timestamp;
+    uint64_t last_timestamp;
+} BootInfo;
+
+static void boot_info_free(BootInfo *info) {
+    g_free(info->boot_id);
+    g_free(info->version);
+    g_free(info);
+}
+
+static int boot_info_cmp(gconstpointer a, gconstpointer b) {
+    const BootInfo *entry1 = a;
+    const BootInfo *entry2 = b;
+
+    if (entry1->first_timestamp < entry2->first_timestamp) return -1;
+    if (entry1->first_timestamp > entry2->first_timestamp) return 1;
+    return 0;
+}
+
+char* get_version_from_journal_entry(const char *message) {
+    g_autoptr(GRegex) regex = NULL;
+    g_autoptr(GMatchInfo) match_info = NULL;
+    g_autoptr(GError) error = NULL;
+    int start_pos = 0, end_pos = 0;
+
+    g_return_val_if_fail(message != NULL, g_strdup("unknown"));
+
+    regex = g_regex_new("Linux version ([a-zA-Z0-9.-]+)", G_REGEX_OPTIMIZE, G_REGEX_MATCH_DEFAULT, &error);
+    if (error) {
+        g_critical("Regex compilation failed: %s", error->message);
+        return g_strdup("unknown");
+    }
+
+    g_return_val_if_fail(regex != NULL, g_strdup("unknown"));
+
+    g_regex_match(regex, message, G_REGEX_MATCH_DEFAULT, &match_info);
+
+    if (!g_match_info_matches(match_info))
+        return g_strdup("unknown");
+
+    g_match_info_fetch_pos(match_info, 1, &start_pos, &end_pos);
+
+    return g_strndup(message + start_pos, end_pos - start_pos);
+}
+
+char* format_timestamp(uint64_t timestamp) {
+    g_autoptr(GDateTime) time = NULL;
+
+    if (timestamp == 0) {
+        return g_strdup ("-");
+    }
+
+    time = g_date_time_new_from_unix_local_usec(timestamp);
+    return g_date_time_format(time, "%Y-%m-%d %H:%M");
+}
+
+static void print_boot(BootInfo *info, gpointer user_data) {
+    g_return_if_fail(info != NULL);
+
+    g_autofree char *start = format_timestamp(info->first_timestamp);
+    g_autofree char *end = format_timestamp(info->last_timestamp);
+    g_print("%s\t%s\t%s\t%s\n", start, end, info->boot_id, info->version);
+}
+
+static int list_boots_and_kernels(sd_journal *j) {
+    const void *data;
+    const size_t prefix_len = strlen("_BOOT_ID=");
+    size_t len;
+    int r;
+    g_autoptr(GPtrArray) boot_ids = NULL;
+    g_autoptr(GPtrArray) boot_array = NULL;
+
+    boot_ids = g_ptr_array_new_with_free_func(g_free);
+
+    r = sd_journal_query_unique(j, "_BOOT_ID");
+    if (r < 0) {
+        g_printerr("Failed to query _BOOT_ID identifier: %s\n", strerror(-r));
+        return 1;
+    }
+    SD_JOURNAL_FOREACH_UNIQUE(j, data, len) {
+        g_ptr_array_add(boot_ids, g_strndup((const char *)data, len));
+    }
+
+    boot_array = g_ptr_array_new_full(boot_ids->len, (GDestroyNotify)boot_info_free);
+
+    sd_journal_flush_matches(j);
+
+    for (size_t i = 0; i < boot_ids->len; i++) {
+        uint64_t first_ts = 0, last_ts = 0;
+        size_t msg_len;
+        const char *message = NULL;
+        const char *match = g_ptr_array_index(boot_ids, i);
+        g_autofree char* version = NULL;
+        g_autofree char *boot_id = NULL;
+
+        // Strip _BOOT_ID= from the message
+        boot_id = g_strndup(match + prefix_len, 32);
+
+        sd_journal_add_match(j, match, strlen(match));
+
+        r = sd_journal_seek_head(j);
+        if (r >= 0) {
+            r = sd_journal_next(j);
+            if (r > 0) {
+                sd_journal_get_realtime_usec(j, &first_ts);
+            }
+        }
+
+        r = sd_journal_get_data(j, "MESSAGE", (const void **)&message, &msg_len);
+        if (r >= 0 && message != NULL && msg_len > 0)
+            version = get_version_from_journal_entry(message);
+        else
+            version = g_strdup("unknown");
+
+        r = sd_journal_seek_tail(j);
+        if (r >= 0) {
+            r = sd_journal_previous(j);
+            if (r > 0) {
+                sd_journal_get_realtime_usec(j, &last_ts);
+            }
+        }
+
+        sd_journal_flush_matches(j);
+
+        BootInfo *boot_info = g_new0 (BootInfo, 1);
+        boot_info->boot_id = g_steal_pointer(&boot_id);
+        boot_info->version = g_steal_pointer(&version);
+        boot_info->first_timestamp = first_ts;
+        boot_info->last_timestamp = last_ts;
+
+        g_ptr_array_add(boot_array, boot_info);
+    }
+
+    g_ptr_array_sort_values(boot_array, (GCompareFunc)boot_info_cmp);
+    g_ptr_array_foreach(boot_array, (GFunc)print_boot, NULL);
+
+    return 0;
+}
+
 // helper to print errors on stderr
 // if we're in json mode, print closing json body if possible
 static void print_error_and_exit(const char *fmt, ...) {
@@ -471,6 +614,7 @@ _Noreturn static void usage(char *error) {
         "  -J\t\t\tprint as json with one object of separate fields per entry\n"
         "  -I\t\t\twith -J, also emit a record listing the distinct syslog identifiers\n"
         "  -U\t\t\twith -J, also emit a record listing the distinct systemd units\n"
+        "  -B\t\t\tList boots. The columns are: the date of the first entry in the boot and last entry, the boot id, and the kernel version\n"
         "  -h\t\t\tthis help\n"
         "\n"
         "Passing no range option will dump all the available journal\n"
@@ -711,11 +855,12 @@ int main(int argc, char *argv[]) {
     bool kernel = false;
     bool list_identifiers = false;
     bool list_units = false;
+    bool list_boots = false;
     int c;
 
     progname = argv[0];
 
-    while ((c = getopt(argc, argv, "b:e:d:n:f:t:p:i:u:jJIUkh")) != -1) {
+    while ((c = getopt(argc, argv, "b:e:d:n:f:t:p:i:u:jJIUkh:B")) != -1) {
         switch (c) {
         case 'b':
             begin = arg_to_timestamp_usec(optarg);
@@ -762,6 +907,9 @@ int main(int argc, char *argv[]) {
         case 'U':
             list_units = true;
             break;
+        case 'B':
+            list_boots = true;
+            break;
         case 'h':
             usage(NULL);
         case '?':
@@ -816,6 +964,10 @@ int main(int argc, char *argv[]) {
         return 1;
     }
 
+    if (list_boots) {
+        return list_boots_and_kernels(j);
+    }
+
     // restrict the traversal before seeking. Each filter is a separate group, conjoined (AND) with
     // the others; within a group matches are OR'd (same field, or an explicit disjunction for the
     // unit), so the groups must be split by a conjunction
-- 
2.47.3





^ permalink raw reply related	[flat|nested] 3+ messages in thread

* [PATCH manager v2 2/2] report: list recent reboot events and their kernel version
  2026-08-05  9:45 [PATCH manager/proxmox-mini-journalreader v2 0/2] report: list recent reboot events and their kernel version Maximiliano Sandoval
  2026-08-05  9:45 ` [PATCH proxmox-mini-journalreader v2 1/2] add -B parameter to lists boots Maximiliano Sandoval
@ 2026-08-05  9:45 ` Maximiliano Sandoval
  1 sibling, 0 replies; 3+ messages in thread
From: Maximiliano Sandoval @ 2026-08-05  9:45 UTC (permalink / raw)
  To: pve-devel

Based on the most recent reboot events, it is possible to determine whether and
when a crash, fencing, or reboot occurred and with which kernel version the node
booted.

Signed-off-by: Maximiliano Sandoval <m.sandoval@proxmox.com>
---
 PVE/Report.pm | 1 +
 1 file changed, 1 insertion(+)

diff --git a/PVE/Report.pm b/PVE/Report.pm
index e255d538f..b0164be94 100644
--- a/PVE/Report.pm
+++ b/PVE/Report.pm
@@ -37,6 +37,7 @@ my $init_report_cmds = sub {
                 'hostname',
                 'date -R',
                 'cat /proc/cmdline',
+                '/usr/libexec/proxmox-mini-journalreader -B | tail -n 10',
                 'pveversion --verbose',
                 'cat /etc/hosts',
                 'pvesubscription get',
-- 
2.47.3





^ permalink raw reply related	[flat|nested] 3+ messages in thread

end of thread, other threads:[~2026-08-05  9:45 UTC | newest]

Thread overview: 3+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-05  9:45 [PATCH manager/proxmox-mini-journalreader v2 0/2] report: list recent reboot events and their kernel version Maximiliano Sandoval
2026-08-05  9:45 ` [PATCH proxmox-mini-journalreader v2 1/2] add -B parameter to lists boots Maximiliano Sandoval
2026-08-05  9:45 ` [PATCH manager v2 2/2] report: list recent reboot events and their kernel version Maximiliano Sandoval

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal