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

Network: Return ACL logs from syslogs when the OVN controller is deployed in MicroOVN #14327

Open
wants to merge 4 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
1 change: 1 addition & 0 deletions doc/.custom_wordlist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,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
89 changes: 84 additions & 5 deletions lxd/network/acl/acl_ovn.go
gabrielmougard marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"net"
"strconv"
"strings"
"time"

Expand Down Expand Up @@ -1042,7 +1044,23 @@ 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 {
parseLogTimeFromFields := func(fields []string) (time.Time, error) {
return time.Parse(time.RFC3339, fields[0])
}

parseLogTimeFromTimestamp := func(timestamp string) (time.Time, error) {
tsInt, err := strconv.ParseInt(timestamp, 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(input, "|")

// Skip unknown formatting.
Expand Down Expand Up @@ -1071,10 +1089,18 @@ 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
if timestamp == "" {
gabrielmougard marked this conversation as resolved.
Show resolved Hide resolved
logTime, err = parseLogTimeFromFields(fields)
if err != nil {
return ""
}
} else {
logTime, err = parseLogTimeFromTimestamp(timestamp)
if err != nil {
return ""
}
}

// Get the protocol.
Expand Down Expand Up @@ -1131,3 +1157,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(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",
gabrielmougard marked this conversation as resolved.
Show resolved Hide resolved
"--boot", "0",
"--case-sensitive",
"--grep", prefix,
"--output-fields", "MESSAGE",
"-n", "1000",
"-o", "json",
gabrielmougard marked this conversation as resolved.
Show resolved Hide resolved
}

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
}
63 changes: 40 additions & 23 deletions lxd/network/acl/driver_common.go
gabrielmougard marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -749,35 +749,52 @@ 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

// 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")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please break out the logic to detect if microovn is being used into a different function in network_utils.go

if err == nil && strings.HasSuffix(targetPath, "/microovn/chassis/switch") {
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: %v\n", err)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shouldnt use \n in returned errors.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should use %w when wrapping errors

}
} 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.
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
Loading