Skip to content

Commit

Permalink
Network: Return ACL logs from syslogs when the OVN controller is depl…
Browse files Browse the repository at this point in the history
…oyed in MicroOVN (#14327)

closes #12836

We need to remember to enable the log tests for microovn on
canonical/lxd-ci#368 when this is merged
  • Loading branch information
tomponline authored Jan 7, 2025
2 parents 1ee0b92 + 026d323 commit 5f3c0fa
Show file tree
Hide file tree
Showing 7 changed files with 143 additions and 30 deletions.
1 change: 1 addition & 0 deletions doc/.custom_wordlist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ MiB
Mibit
MicroCeph
MicroCloud
MicroOVN
MII
MinIO
MITM
Expand Down
4 changes: 4 additions & 0 deletions doc/howto/network_ovn_setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,10 @@ See the linked YouTube video for the complete tutorial using four machines.
lxc config set network.ovn.northbound_connection <ovn-northd-nb-db>
```{note}
If you are using a MicroOVN deployment, pass the value of the MicroOVN node IP address you want to target. Prefix the IP address with `ssl:`, and suffix it with the `:6641` port number that corresponds to the OVN central service within MicroOVN.
```

1. Finally, create the actual OVN network (on the first machine):

lxc network create my-ovn --type=ovn
Expand Down
4 changes: 3 additions & 1 deletion lxd/network/acl/acl_interface.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package acl

import (
"context"

"github.com/canonical/lxd/lxd/cluster/request"
"github.com/canonical/lxd/lxd/state"
"github.com/canonical/lxd/shared/api"
Expand All @@ -19,7 +21,7 @@ type NetworkACL interface {
UsedBy() ([]string, error)

// GetLog.
GetLog(clientType request.ClientType) (string, error)
GetLog(ctx context.Context, clientType request.ClientType) (string, error)

// Internal validation.
validateName(name string) error
Expand Down
90 changes: 85 additions & 5 deletions lxd/network/acl/acl_ovn.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package acl

import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net"
"strconv"
"strings"
"time"

Expand Down Expand Up @@ -1041,9 +1044,26 @@ type ovnLogEntry struct {
Action string `json:"action"`
}

// 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 {
fields := strings.Split(input, "|")
// ovnParseLogEntry takes a log line (that comes from either an ovn controller log file or from the syslogs)
// and expected ACL prefix and returns a re-formated log entry if matching.
// The 'timestamp' string is in microseconds format. If empty, the timestamp is extracted from the log entry.
func ovnParseLogEntry(logline string, syslogTimestamp string, prefix string) string {
parseLogTimeFromFields := func(fields []string) (time.Time, error) {
return time.Parse(time.RFC3339, fields[0])
}

parseLogTimeFromTimestamp := func(syslogTimestamp string) (time.Time, error) {
tsInt, err := strconv.ParseInt(syslogTimestamp, 10, 64)
if err != nil {
return time.Time{}, fmt.Errorf("Failed to parse timestamp: %w", err)
}

// The provided timestamp is in microseconds and need to be converted to nanoseconds.
tsNs := tsInt * 1000
return time.Unix(0, tsNs).UTC(), nil
}

fields := strings.Split(logline, "|")

// Skip unknown formatting.
if len(fields) != 5 {
Expand Down Expand Up @@ -1071,8 +1091,14 @@ func ovnParseLogEntry(input string, prefix string) string {
return ""
}

// Parse the timestamp.
logTime, err := time.Parse(time.RFC3339, fields[0])
var logTime time.Time
var err error
if syslogTimestamp == "" {
logTime, err = parseLogTimeFromFields(fields)
} else {
logTime, err = parseLogTimeFromTimestamp(syslogTimestamp)
}

if err != nil {
return ""
}
Expand Down Expand Up @@ -1131,3 +1157,57 @@ 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.
// Also, we chose to output the last 1000 entries to avoid overloading the system with too many log entries.
func ovnParseLogEntriesFromJournald(ctx context.Context, systemdUnitName string, filter 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", filter,
"--output-fields", "MESSAGE",
"-n", "1000",
"-o", "json",
}

stdout := bytes.Buffer{}
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(&stdout)
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, filter)
if logEntry == "" {
continue
}

logEntries = append(logEntries, logEntry)
}

return logEntries, nil
}
60 changes: 37 additions & 23 deletions lxd/network/acl/driver_common.go
Original file line number Diff line number Diff line change
Expand Up @@ -749,35 +749,49 @@ func (d *common) Delete() error {
}

// GetLog gets the ACL log.
func (d *common) GetLog(clientType request.ClientType) (string, error) {
func (d *common) GetLog(ctx context.Context, 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

// Open the log file.
logFile, err := os.Open(logPath)
if err != nil {
return "", fmt.Errorf("Couldn't open OVN log file: %w", err)
}

defer func() { _ = logFile.Close() }()
if shared.IsMicroOVNUsed() {
prefix := fmt.Sprintf("lxd_acl%d-", d.id)
logEntries, err = ovnParseLogEntriesFromJournald(ctx, "snap.microovn.chassis.service", prefix)
if err != nil {
return "", fmt.Errorf("Failed to get OVN log entries from syslog: %w", err)
}
} else {
// Else, if the current LXD deployment does not use MicroOVN,
// then try to read the OVN controller log file directly (a standalone OVN controller might be built-in with LXD).
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")
}

logEntries := []string{}
scanner := bufio.NewScanner(logFile)
for scanner.Scan() {
logEntry := ovnParseLogEntry(scanner.Text(), fmt.Sprintf("lxd_acl%d-", d.id))
if logEntry == "" {
continue
// Open the log file.
logFile, err := os.Open(logPath)
if err != nil {
return "", fmt.Errorf("Failed to 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(), "", prefix)
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
2 changes: 1 addition & 1 deletion lxd/network_acls.go
Original file line number Diff line number Diff line change
Expand Up @@ -637,7 +637,7 @@ func networkACLLogGet(d *Daemon, r *http.Request) response.Response {
}

clientType := clusterRequest.UserAgentClientType(r.Header.Get("User-Agent"))
log, err := netACL.GetLog(clientType)
log, err := netACL.GetLog(r.Context(), clientType)
if err != nil {
return response.SmartError(err)
}
Expand Down
12 changes: 12 additions & 0 deletions shared/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -1514,3 +1514,15 @@ func ApplyDeviceOverrides(localDevices map[string]map[string]string, profileDevi

return localDevices, nil
}

// IsMicroOVNUsed returns whether the current LXD deployment is using a built-in openvswitch
// or is connected to MicroOVN, which in this case, would make `/run/openvswitch` a symlink to
// `/var/snap/lxd/common/microovn/chassis/switch`.
func IsMicroOVNUsed() bool {
targetPath, err := os.Readlink("/run/openvswitch")
if err == nil && strings.HasSuffix(targetPath, "/microovn/chassis/switch") {
return true
}

return false
}

0 comments on commit 5f3c0fa

Please sign in to comment.