From 62f251d661b2930e0189a96df7f50bcbeffd0e86 Mon Sep 17 00:00:00 2001 From: Gabriel Mougard Date: Mon, 2 Dec 2024 14:04:24 +0100 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 | 54 ++++++++++++++++++++++++++++ lxd/network/acl/driver_common.go | 61 ++++++++++++++++++++------------ 2 files changed, 93 insertions(+), 22 deletions(-) diff --git a/lxd/network/acl/acl_ovn.go b/lxd/network/acl/acl_ovn.go index e5b846a351fc..f61f2d183c13 100644 --- a/lxd/network/acl/acl_ovn.go +++ b/lxd/network/acl/acl_ovn.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "io" "net" "strconv" "strings" @@ -1156,3 +1157,56 @@ func ovnParseLogEntry(input string, timestamp 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(ctx context.Context, 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(ctx, 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 +} diff --git a/lxd/network/acl/driver_common.go b/lxd/network/acl/driver_common.go index 332da23aad13..b36f5d33d12e 100644 --- a/lxd/network/acl/driver_common.go +++ b/lxd/network/acl/driver_common.go @@ -751,33 +751,50 @@ func (d *common) Delete() error { // GetLog gets the ACL log. func (d *common) GetLog(clientType request.ClientType) (string, error) { // ACLs aren't specific to a particular network type but the log only works with OVN. - logPath := shared.HostPath("/var/log/ovn/ovn-controller.log") - if !shared.PathExists(logPath) { - return "", fmt.Errorf("Only OVN log entries may be retrieved at this time") - } + var logEntries []string + var err error + + // First, check if we can resolve the /run/openvswitch symlink to determine if OVN is in use. + // This is used in case of a MicroOVN deployment is interfaced with LXD. + targetPath, err := os.Readlink("/run/openvswitch") + if err == nil && strings.HasSuffix(targetPath, "/microovn/chassis/switch") { + prefix := fmt.Sprintf("lxd_acl%d-", d.id) + logEntries, err = ovnParseLogEntriesFromJournald(context.TODO(), "snap.microovn.chassis.service", prefix) + if err != nil { + return "", fmt.Errorf("Failed to get OVN log entries from syslog: %v\n", err) + } + } else { + // Else, if the /run/openvswitch symlink doesn't exist (which might be the case for a non-snap installation of LXD), + // then try to read the OVN controller log file directly. + logEntries = []string{} + prefix := fmt.Sprintf("lxd_acl%d-", d.id) + logPath := shared.HostPath("/var/log/ovn/ovn-controller.log") + if !shared.PathExists(logPath) { + return "", fmt.Errorf("Only OVN log entries may be retrieved at this time") + } - // Open the log file. - logFile, err := os.Open(logPath) - if err != nil { - return "", fmt.Errorf("Couldn't open OVN log file: %w", err) - } + // Open the log file. + logFile, err := os.Open(logPath) + if err != nil { + return "", fmt.Errorf("Failed to open OVN log file: %w", err) + } - defer func() { _ = logFile.Close() }() + defer func() { _ = logFile.Close() }() - logEntries := []string{} - scanner := bufio.NewScanner(logFile) - for scanner.Scan() { - logEntry := ovnParseLogEntry(scanner.Text(), "", fmt.Sprintf("lxd_acl%d-", d.id)) - if logEntry == "" { - continue - } + scanner := bufio.NewScanner(logFile) + for scanner.Scan() { + logEntry := ovnParseLogEntry(scanner.Text(), "", prefix) + if logEntry == "" { + continue + } - logEntries = append(logEntries, logEntry) - } + logEntries = append(logEntries, logEntry) + } - err = scanner.Err() - if err != nil { - return "", fmt.Errorf("Failed to read OVN log file: %w", err) + err = scanner.Err() + if err != nil { + return "", fmt.Errorf("Failed to read OVN log file: %w", err) + } } // Aggregates the entries from the rest of the cluster.