From 28a1c705098e679b6643882f4f5a8faab52006ad Mon Sep 17 00:00:00 2001 From: Gabriel Mougard Date: Wed, 23 Oct 2024 11:53:02 +0200 Subject: [PATCH] lxd/network/acl: Read OVN logs from systemd journal In the case of an OVN controller being deployed as part of a MicroOVN deployment, the OVN controller logs are stored in MicroOVN's snap syslog. The LXD snap should have root access, which means that it should be authorized (this is being tested) to read the OVN controller logs. Signed-off-by: Gabriel Mougard --- lxd/network/acl/acl_ovn.go | 82 +++++++++++++++++++++++++++++++++++--- 1 file changed, 77 insertions(+), 5 deletions(-) diff --git a/lxd/network/acl/acl_ovn.go b/lxd/network/acl/acl_ovn.go index 0a3b19b22f7e..b2f6947ee869 100644 --- a/lxd/network/acl/acl_ovn.go +++ b/lxd/network/acl/acl_ovn.go @@ -4,7 +4,9 @@ import ( "context" "encoding/json" "fmt" + "io" "net" + "strconv" "strings" "time" @@ -1042,7 +1044,8 @@ type ovnLogEntry struct { } // ovnParseLogEntry takes a log line and expected ACL prefix and returns a re-formated log entry if matching. -func ovnParseLogEntry(input string, prefix string) string { +// The 'timestamp' string is in microseconds format. If empty, the timestamp is extracted from the log entry. +func ovnParseLogEntry(input string, timestamp string, prefix string) string { fields := strings.Split(input, "|") // Skip unknown formatting. @@ -1071,10 +1074,26 @@ func ovnParseLogEntry(input string, prefix string) string { return "" } - // Parse the timestamp. - logTime, err := time.Parse(time.RFC3339, fields[0]) - if err != nil { - return "" + var logTime time.Time + var err error + // When reading from a log file, the timestamp is provided as a message parameter. + if timestamp == "" { + // Parse the timestamp. + logTime, err = time.Parse(time.RFC3339, fields[0]) + if err != nil { + return "" + } + } else { + // Else, if from syslog, the timestamp is provided as a separate field + // which is passed separately. + tsInt, err := strconv.ParseInt(timestamp, 10, 64) + if err != nil { + return "" + } + + // The provided timestamp is in microseconds and need to be converted to nanoseconds. + tsNs := tsInt * 1000 + logTime = time.Unix(0, tsNs).UTC() } // Get the protocol. @@ -1131,3 +1150,56 @@ func ovnParseLogEntry(input string, prefix string) string { return string(out) } + +// ovnParseLogEntriesFromJournald reads the OVN log entries from the systemd journal and returns them as a list of string entries. +func ovnParseLogEntriesFromJournald(systemdUnitName string, prefix string) ([]string, error) { + var logEntries []string + cmd := []string{ + "/usr/bin/journalctl", + "--unit", systemdUnitName, + "--directory", shared.HostPath("/var/log/journal"), + "--no-pager", + "--boot", "0", + "--case-sensitive", + "--grep", prefix, + "--output-fields", "MESSAGE", + "-n", "1000", + "-o", "json", + } + + stdout := strings.Builder{} + err := shared.RunCommandWithFds(context.TODO(), nil, &stdout, cmd[0], cmd[1:]...) + if err != nil { + return nil, fmt.Errorf("Failed to run journalctl to fetch OVN ACL logs: %w", err) + } + + decoder := json.NewDecoder(strings.NewReader(stdout.String())) + for { + var sdLogEntry map[string]any + err = decoder.Decode(&sdLogEntry) + if err == io.EOF { + break + } else if err != nil { + return nil, fmt.Errorf("Failed to parse log entry: %w", err) + } + + message, ok := sdLogEntry["MESSAGE"].(string) + if !ok { + continue + } + + timestamp, ok := sdLogEntry["__REALTIME_TIMESTAMP"].(string) + if !ok { + continue + } + + logEntry := ovnParseLogEntry(message, timestamp, prefix) + if logEntry == "" { + continue + } + + logEntries = append(logEntries, logEntry) + } + + return logEntries, nil +}