Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

lxd/network/acl: return ACL logs from syslogs when the OVN controller is deployed in MicroOVN #14327

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 72 additions & 5 deletions lxd/network/acl/acl_ovn.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"encoding/json"
"fmt"
"net"
"strconv"
"strings"
"time"

Expand Down Expand Up @@ -1042,7 +1043,7 @@ 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 {
func ovnParseLogEntry(input string, timestamp string, prefix string) string {
fields := strings.Split(input, "|")

// Skip unknown formatting.
Expand Down Expand Up @@ -1071,10 +1072,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.
Expand Down Expand Up @@ -1131,3 +1148,53 @@ func ovnParseLogEntry(input string, prefix string) string {

return string(out)
}

// ovnParseLogEntriesFromSyslog reads the OVN log entries from the systemd journal and returns them as a list of string entries.
func ovnParseLogEntriesFromSyslog(l logger.Logger, systemdUnitName string, prefix string) ([]string, error) {
var logEntries []string
res, err := shared.RunCommand(
"/usr/bin/journalctl",
"--unit", systemdUnitName,
"--directory", "/var/lib/snapd/hostfs/var/log/journal",
"--no-pager",
"--grep", "acl_log",
"-n", "5000",
"-o", "json",
)
if err != nil {
return nil, fmt.Errorf("Failed to read journal entries: %v", err)
}

lines := strings.Split(res, "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" {
continue
}

var sdLogEntry map[string]any
err := json.Unmarshal([]byte(line), &sdLogEntry)
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
}
57 changes: 37 additions & 20 deletions lxd/network/acl/driver_common.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"net"
"os"
"regexp"
"sort"
"strings"
"sync"
Expand Down Expand Up @@ -748,36 +749,52 @@ func (d *common) Delete() error {
})
}

var microovnTargetChassisPath = regexp.MustCompile(`^/var/snap/lxd/[0-9]+/microovn/chassis/switch`)

// 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")
logEntries := []string{}
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)
}
// Check if this is OVN builtin of if OVN is provided by a MicroOVN deployment.
targetPath, err := os.Readlink("/run/openvswitch")
if err != nil {
return "", fmt.Errorf("Failed to read symlink while checking for OVN logs: %v\n", err)
}

defer func() { _ = logFile.Close() }()
if !microovnTargetChassisPath.MatchString(targetPath) {
// This is a builtin OVN deployment (snap-based or not) but the log file doesn't exist. This is an error.
return "", fmt.Errorf("Only OVN log entries may be retrieved at this time")
}

logEntries := []string{}
scanner := bufio.NewScanner(logFile)
for scanner.Scan() {
logEntry := ovnParseLogEntry(scanner.Text(), fmt.Sprintf("lxd_acl%d-", d.id))
if logEntry == "" {
continue
logEntries, err = ovnParseLogEntriesFromSyslog(d.logger, "snap.microovn.chassis.service", fmt.Sprintf("lxd_acl%d-", d.id))
if err != nil {
return "", fmt.Errorf("Failed to get OVN log entries from syslog: %v\n", err)
}
} else {
// Open the log file.
logFile, err := os.Open(logPath)
if err != nil {
return "", fmt.Errorf("Couldn't open OVN log file: %w", err)
}

logEntries = append(logEntries, logEntry)
}
defer func() { _ = logFile.Close() }()

err = scanner.Err()
if err != nil {
return "", fmt.Errorf("Failed to read OVN log file: %w", err)
scanner := bufio.NewScanner(logFile)
for scanner.Scan() {
logEntry := ovnParseLogEntry(scanner.Text(), "", fmt.Sprintf("lxd_acl%d-", d.id))
if logEntry == "" {
continue
}

logEntries = append(logEntries, logEntry)
}

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.
Expand Down
Loading