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: Add OVS bridge information #15044

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
223 changes: 202 additions & 21 deletions lxd/network/openvswitch/ovs.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package openvswitch

import (
"context"
"fmt"
"net"
"os/exec"
"strconv"
"strings"
"sync"
"time"

"github.com/canonical/lxd/lxd/ip"
"github.com/canonical/lxd/shared"
Expand Down Expand Up @@ -43,7 +46,9 @@ func (o *OVS) Installed() bool {

// BridgeExists returns true if OVS bridge exists.
func (o *OVS) BridgeExists(bridgeName string) (bool, error) {
_, err := shared.RunCommand("ovs-vsctl", "br-exists", bridgeName)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
_, err := shared.RunCommandContext(ctx, "ovs-vsctl", "br-exists", bridgeName)
if err != nil {
runErr, ok := err.(shared.RunError)
if ok {
Expand Down Expand Up @@ -79,7 +84,9 @@ func (o *OVS) BridgeAdd(bridgeName string, mayExist bool, hwaddr net.HardwareAdd
args = append(args, "--", "set", "int", bridgeName, fmt.Sprintf(`mtu_request=%d`, mtu))
}

_, err := shared.RunCommand("ovs-vsctl", args...)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
_, err := shared.RunCommandContext(ctx, "ovs-vsctl", args...)
if err != nil {
return err
}
Expand All @@ -89,7 +96,9 @@ func (o *OVS) BridgeAdd(bridgeName string, mayExist bool, hwaddr net.HardwareAdd

// BridgeDelete deletes an OVS bridge.
func (o *OVS) BridgeDelete(bridgeName string) error {
_, err := shared.RunCommand("ovs-vsctl", "del-br", bridgeName)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
_, err := shared.RunCommandContext(ctx, "ovs-vsctl", "del-br", bridgeName)
if err != nil {
return err
}
Expand All @@ -106,8 +115,9 @@ func (o *OVS) BridgePortAdd(bridgeName string, portName string, mayExist bool) e
}

args = append(args, "add-port", bridgeName, portName)

_, err := shared.RunCommand("ovs-vsctl", args...)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
_, err := shared.RunCommandContext(ctx, "ovs-vsctl", args...)
if err != nil {
return err
}
Expand All @@ -117,7 +127,9 @@ func (o *OVS) BridgePortAdd(bridgeName string, portName string, mayExist bool) e

// BridgePortDelete deletes a port from the bridge (if already detached does nothing).
func (o *OVS) BridgePortDelete(bridgeName string, portName string) error {
_, err := shared.RunCommand("ovs-vsctl", "--if-exists", "del-port", bridgeName, portName)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
_, err := shared.RunCommandContext(ctx, "ovs-vsctl", "--if-exists", "del-port", bridgeName, portName)
if err != nil {
return err
}
Expand All @@ -127,7 +139,9 @@ func (o *OVS) BridgePortDelete(bridgeName string, portName string) error {

// BridgePortSet sets port options.
func (o *OVS) BridgePortSet(portName string, options ...string) error {
_, err := shared.RunCommand("ovs-vsctl", append([]string{"set", "port", portName}, options...)...)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
_, err := shared.RunCommandContext(ctx, "ovs-vsctl", append([]string{"set", "port", portName}, options...)...)
if err != nil {
return err
}
Expand All @@ -138,16 +152,20 @@ func (o *OVS) BridgePortSet(portName string, options ...string) error {
// InterfaceAssociateOVNSwitchPort removes any existing OVS ports associated to the specified ovnSwitchPortName
// and then associates the specified interfaceName to the OVN switch port.
func (o *OVS) InterfaceAssociateOVNSwitchPort(interfaceName string, ovnSwitchPortName OVNSwitchPort) error {
ctx1, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
// Clear existing ports that were formerly associated to ovnSwitchPortName.
existingPorts, err := shared.RunCommand("ovs-vsctl", "--format=csv", "--no-headings", "--data=bare", "--colum=name", "find", "interface", fmt.Sprintf("external-ids:iface-id=%s", string(ovnSwitchPortName)))
existingPorts, err := shared.RunCommandContext(ctx1, "ovs-vsctl", "--format=csv", "--no-headings", "--data=bare", "--colum=name", "find", "interface", fmt.Sprintf("external-ids:iface-id=%s", string(ovnSwitchPortName)))
if err != nil {
return err
}

existingPorts = strings.TrimSpace(existingPorts)
if existingPorts != "" {
for _, port := range strings.Split(existingPorts, "\n") {
_, err = shared.RunCommand("ovs-vsctl", "del-port", port)
ctx2, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
_, err = shared.RunCommandContext(ctx2, "ovs-vsctl", "del-port", port)
if err != nil {
return err
}
Expand All @@ -159,8 +177,9 @@ func (o *OVS) InterfaceAssociateOVNSwitchPort(interfaceName string, ovnSwitchPor
_ = link.Delete()
}
}

_, err = shared.RunCommand("ovs-vsctl", "set", "interface", interfaceName, fmt.Sprintf("external_ids:iface-id=%s", string(ovnSwitchPortName)))
ctx3, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
_, err = shared.RunCommandContext(ctx3, "ovs-vsctl", "set", "interface", interfaceName, fmt.Sprintf("external_ids:iface-id=%s", string(ovnSwitchPortName)))
if err != nil {
return err
}
Expand All @@ -170,7 +189,9 @@ func (o *OVS) InterfaceAssociateOVNSwitchPort(interfaceName string, ovnSwitchPor

// InterfaceAssociatedOVNSwitchPort returns the OVN switch port associated to the OVS interface.
func (o *OVS) InterfaceAssociatedOVNSwitchPort(interfaceName string) (OVNSwitchPort, error) {
ovnSwitchPort, err := shared.RunCommand("ovs-vsctl", "get", "interface", interfaceName, "external_ids:iface-id")
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
ovnSwitchPort, err := shared.RunCommandContext(ctx, "ovs-vsctl", "get", "interface", interfaceName, "external_ids:iface-id")
if err != nil {
return "", err
}
Expand All @@ -180,11 +201,13 @@ func (o *OVS) InterfaceAssociatedOVNSwitchPort(interfaceName string) (OVNSwitchP

// ChassisID returns the local chassis ID.
func (o *OVS) ChassisID() (string, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
// ovs-vsctl's get command doesn't support its --format flag, so we always get the output quoted.
// However ovs-vsctl's find and list commands don't support retrieving a single column's map field.
// And ovs-vsctl's JSON output is unfriendly towards statically typed languages as it mixes data types
// in a slice. So stick with "get" command and use Go's strconv.Unquote to return the actual values.
chassisID, err := shared.RunCommand("ovs-vsctl", "get", "open_vswitch", ".", "external_ids:system-id")
chassisID, err := shared.RunCommandContext(ctx, "ovs-vsctl", "get", "open_vswitch", ".", "external_ids:system-id")
if err != nil {
return "", err
}
Expand All @@ -200,11 +223,13 @@ func (o *OVS) ChassisID() (string, error) {

// OVNEncapIP returns the enscapsulation IP used for OVN underlay tunnels.
func (o *OVS) OVNEncapIP() (net.IP, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
// ovs-vsctl's get command doesn't support its --format flag, so we always get the output quoted.
// However ovs-vsctl's find and list commands don't support retrieving a single column's map field.
// And ovs-vsctl's JSON output is unfriendly towards statically typed languages as it mixes data types
// in a slice. So stick with "get" command and use Go's strconv.Unquote to return the actual values.
encapIPStr, err := shared.RunCommand("ovs-vsctl", "get", "open_vswitch", ".", "external_ids:ovn-encap-ip")
encapIPStr, err := shared.RunCommandContext(ctx, "ovs-vsctl", "get", "open_vswitch", ".", "external_ids:ovn-encap-ip")
if err != nil {
return nil, err
}
Expand All @@ -225,11 +250,13 @@ func (o *OVS) OVNEncapIP() (net.IP, error) {

// OVNBridgeMappings gets the current OVN bridge mappings.
func (o *OVS) OVNBridgeMappings(bridgeName string) ([]string, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
// ovs-vsctl's get command doesn't support its --format flag, so we always get the output quoted.
// However ovs-vsctl's find and list commands don't support retrieving a single column's map field.
// And ovs-vsctl's JSON output is unfriendly towards statically typed languages as it mixes data types
// in a slice. So stick with "get" command and use Go's strconv.Unquote to return the actual values.
mappings, err := shared.RunCommand("ovs-vsctl", "--if-exists", "get", "open_vswitch", ".", "external-ids:ovn-bridge-mappings")
mappings, err := shared.RunCommandContext(ctx, "ovs-vsctl", "--if-exists", "get", "open_vswitch", ".", "external-ids:ovn-bridge-mappings")
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -266,8 +293,10 @@ func (o *OVS) OVNBridgeMappingAdd(bridgeName string, providerName string) error

mappings = append(mappings, newMapping)

ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
// Set new mapping string back into OVS database.
_, err = shared.RunCommand("ovs-vsctl", "set", "open_vswitch", ".", fmt.Sprintf("external-ids:ovn-bridge-mappings=%s", strings.Join(mappings, ",")))
_, err = shared.RunCommandContext(ctx, "ovs-vsctl", "set", "open_vswitch", ".", fmt.Sprintf("external-ids:ovn-bridge-mappings=%s", strings.Join(mappings, ",")))
if err != nil {
return err
}
Expand Down Expand Up @@ -296,16 +325,18 @@ func (o *OVS) OVNBridgeMappingDelete(bridgeName string, providerName string) err
}
}

ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
if changed {
if len(newMappings) < 1 {
// Remove mapping key in OVS database.
_, err = shared.RunCommand("ovs-vsctl", "remove", "open_vswitch", ".", "external-ids", "ovn-bridge-mappings")
_, err = shared.RunCommandContext(ctx, "ovs-vsctl", "remove", "open_vswitch", ".", "external-ids", "ovn-bridge-mappings")
if err != nil {
return err
}
} else {
// Set updated mapping string back into OVS database.
_, err = shared.RunCommand("ovs-vsctl", "set", "open_vswitch", ".", fmt.Sprintf("external-ids:ovn-bridge-mappings=%s", strings.Join(newMappings, ",")))
_, err = shared.RunCommandContext(ctx, "ovs-vsctl", "set", "open_vswitch", ".", fmt.Sprintf("external-ids:ovn-bridge-mappings=%s", strings.Join(newMappings, ",")))
if err != nil {
return err
}
Expand All @@ -317,8 +348,10 @@ func (o *OVS) OVNBridgeMappingDelete(bridgeName string, providerName string) err

// BridgePortList returns a list of ports that are connected to the bridge.
func (o *OVS) BridgePortList(bridgeName string) ([]string, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
// Clear existing ports that were formerly associated to ovnSwitchPortName.
portString, err := shared.RunCommand("ovs-vsctl", "list-ports", bridgeName)
portString, err := shared.RunCommandContext(ctx, "ovs-vsctl", "list-ports", bridgeName)
if err != nil {
return nil, err
}
Expand All @@ -337,11 +370,13 @@ func (o *OVS) BridgePortList(bridgeName string) ([]string, error) {

// HardwareOffloadingEnabled returns true if hardware offloading is enabled.
func (o *OVS) HardwareOffloadingEnabled() bool {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
// ovs-vsctl's get command doesn't support its --format flag, so we always get the output quoted.
// However ovs-vsctl's find and list commands don't support retrieving a single column's map field.
// And ovs-vsctl's JSON output is unfriendly towards statically typed languages as it mixes data types
// in a slice. So stick with "get" command and use Go's strconv.Unquote to return the actual values.
offload, err := shared.RunCommand("ovs-vsctl", "--if-exists", "get", "open_vswitch", ".", "other_config:hw-offload")
offload, err := shared.RunCommandContext(ctx, "ovs-vsctl", "--if-exists", "get", "open_vswitch", ".", "other_config:hw-offload")
if err != nil {
return false
}
Expand All @@ -361,7 +396,9 @@ func (o *OVS) HardwareOffloadingEnabled() bool {

// OVNSouthboundDBRemoteAddress gets the address of the southbound ovn database.
func (o *OVS) OVNSouthboundDBRemoteAddress() (string, error) {
result, err := shared.RunCommand("ovs-vsctl", "get", "open_vswitch", ".", "external_ids:ovn-remote")
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
result, err := shared.RunCommandContext(ctx, "ovs-vsctl", "get", "open_vswitch", ".", "external_ids:ovn-remote")
if err != nil {
return "", err
}
Expand All @@ -373,3 +410,147 @@ func (o *OVS) OVNSouthboundDBRemoteAddress() (string, error) {

return addr, nil
}

// GetStpPriority returns the STP priority of the OVS bridge.
// Default STP priority is 32768 (0x8000). This value will be used if the
// priority cannot be retrieved or if STP is disabled.
func GetStpPriority(bridgeName string) (string, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
output, err := shared.RunCommandContext(ctx, "ovs-vsctl", "get", "bridge", bridgeName, "other_config:stp-priority")
// get bridge does not differentiate in it's error codes between variable undefined and other errors, such as bridge not found
// the error output will need to be checked to make sure if stp-priority is not defined (defaults to 0x8000) or if a breaking error happened
if err != nil {
// default stp-priority is used if stp-priority is not defined
if strings.Contains(err.Error(), fmt.Sprintf("no key \"stp-priority\" in Bridge record \"%s\" column other_config", bridgeName)) {
return "8000", nil
}

// All other errors are considered errors
return "8000", err
}

// Convert to hex format
output = strings.TrimSpace(output)
// remove surrounding quotes
output = output[1 : len(output)-1]
stpPriority, err := strconv.Atoi(output)
if err != nil {
return "8000", err
}

hexStp := fmt.Sprintf("%4X", stpPriority)
return hexStp, nil
}

// GenerateOVSBridgeID returns the bridge ID of the OVS bridge.
// The bridge IDs follow the following format <STP priority>.<MAC Address>.
// Check the Bridge ID section on https://www.kernel.org/doc/Documentation/networking/bridge.rst.
// The STP priority is in hexadecimal format just like the MAC address.
func (o *OVS) GenerateOVSBridgeID(bridgeName string) (string, error) {
// get the MAC address
netIf, err := net.InterfaceByName(bridgeName)
if err != nil {
return "None", err
}

bridgeHwID := strings.ReplaceAll(strings.ToLower(netIf.HardwareAddr.String()), ":", "")
// Get the STP priority
stpPriority, err := GetStpPriority(bridgeName)
if err != nil {
return "None", err
}

return stpPriority + "." + bridgeHwID, nil
}

// StpEnabled checks if STP is enabled by looking up the "stp_enable" boolean config variable.
// Returns the value stored in "stp_enable", or false if it is undefined.
func (o *OVS) StpEnabled(bridgeName string) (bool, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
output, err := shared.RunCommandContext(ctx, "ovs-vsctl", "get", "bridge", bridgeName, "stp_enable")
if err != nil {
return false, err
}

output = strings.TrimSpace(output)
return strconv.ParseBool(output)
}

// GetStpForwardDelay returns the STP forward delay in ms. OVS returns the value in seconds, so it needs to be
// converted to ms to satisfy the api.NetworkStateBridge struct which expects the value in ms.
// Default value is 15s.
// Check the "other_config : stp-forward-delay:" section on http://www.openvswitch.org/support/dist-docs/ovs-vswitchd.conf.db.5.txt
func (o *OVS) GetStpForwardDelay(bridgeName string) (uint64, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
output, err := shared.RunCommandContext(ctx, "ovs-vsctl", "get", "bridge", bridgeName, "other_config:stp-forward-delay")
// get bridge does not differentiate in it's error codes between variables undefined and other errors, such as bridge not found
// the error output will need to be checked to make sure if stp-forward-delay is not defined (defaults to 15000) or if a breaking error happened
if err != nil {
// default stp-priority is used if stp-priority is not defined
if strings.Contains(err.Error(), fmt.Sprintf("no key \"stp-forward-delay\" in Bridge record \"%s\" column other_config", bridgeName)) {
return 15 * 1000, nil
}

// All other errors are considered errors
return 15 * 1000, err
}

output = strings.TrimSpace(output)
// remove surrounding quotes
output = output[1 : len(output)-1]
// Convert to uint64
stpFwdDelay, err := strconv.ParseUint(output, 10, 64)
if err != nil {
return 15 * 1000, err
}

return stpFwdDelay * 1000, nil
}

// VlanFilteringEnabled checks if a vlans are enabled on the OVS bridge.
// In OVS, Vlan filtering is enabled when Vlan related settings are configured.
func (o *OVS) VlanFilteringEnabled(bridgeName string) (bool, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
// check if the tag, trunks or vlan_mode fields are populated
output, err := shared.RunCommandContext(ctx, "ovs-vsctl", "get", "port", bridgeName, "tag", "trunks", "vlan_mode")
if err != nil {
return false, err
}

lines := strings.Split(strings.TrimSpace(output), "\n")
for _, line := range lines {
// when no value is defined "[]" is returned
if line != "[]" {
return true, nil
}
}

return false, nil
}

// GetVlanPvid returbs the PVID of the ovs bridge.
// In OVS a PVID of 0 means that the port is not associated with any VLAN.
func (o *OVS) GetVlanPvid(bridgeName string) (uint64, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
output, err := shared.RunCommandContext(ctx, "ovs-vsctl", "get", "port", bridgeName, "tag")
if err != nil {
return 0, err
}

output = strings.TrimSpace(output)
if output == "[]" {
return 0, nil
}

pvid, err := strconv.ParseUint(output, 10, 64)
if err != nil {
return 0, err
}

return pvid, nil
}
Loading
Loading