Skip to content

Commit

Permalink
Merge pull request #1263 from SpiffyEight77/feat/ovn-external-interfaces
Browse files Browse the repository at this point in the history
Allow adding external interfaces to an OVN network
  • Loading branch information
stgraber authored Oct 3, 2024
2 parents 6dff5f0 + 270438a commit 596df89
Show file tree
Hide file tree
Showing 6 changed files with 162 additions and 44 deletions.
4 changes: 4 additions & 0 deletions doc/api-extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -2623,3 +2623,7 @@ This adds a `mode` configuration key on `macvlan` network interfaces which allow
## `storage_lvm_cluster_create`

Allow for creating new LVM cluster pools by setting the `source` to the shared block device.

## `network_ovn_external_interfaces`

This adds support for `bridge.external_interfaces` on OVN networks.
1 change: 1 addition & 0 deletions doc/reference/network_ovn.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ The following configuration options are available for the `ovn` network type:
Key | Type | Condition | Default | Description
:-- | :-- | :-- | :-- | :--
`network` | string | - | - | Uplink network to use for external network access or `none` to keep isolated
`bridge.external_interfaces` | string | - | - | Comma-separated list of unconfigured network interfaces to include in the bridge
`bridge.hwaddr` | string | - | - | MAC address for the bridge
`bridge.mtu` | integer | - | `1442` | Bridge MTU (default allows host to host Geneve tunnels)
`dns.domain` | string | - | `incus` | Domain to advertise to DHCP clients and use for DNS resolution
Expand Down
45 changes: 4 additions & 41 deletions internal/server/network/driver_bridge.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,47 +163,10 @@ func (n *bridge) Validate(config map[string]string) error {
"bgp.ipv4.nexthop": validate.Optional(validate.IsNetworkAddressV4),
"bgp.ipv6.nexthop": validate.Optional(validate.IsNetworkAddressV6),

"bridge.driver": validate.Optional(validate.IsOneOf("native", "openvswitch")),
"bridge.external_interfaces": validate.Optional(func(value string) error {
for _, entry := range strings.Split(value, ",") {
entry = strings.TrimSpace(entry)

// Test for extended configuration of external interface.
entryParts := strings.Split(entry, "/")
if len(entryParts) == 3 {
// The first part is the interface name.
entry = strings.TrimSpace(entryParts[0])
}

err := validate.IsInterfaceName(entry)
if err != nil {
return fmt.Errorf("Invalid interface name %q: %w", entry, err)
}

if len(entryParts) == 3 {
// Check if the parent interface is valid.
parent := strings.TrimSpace(entryParts[1])
err := validate.IsInterfaceName(parent)
if err != nil {
return fmt.Errorf("Invalid interface name %q: %w", parent, err)
}

// Check if the VLAN ID is valid.
vlanID, err := strconv.Atoi(entryParts[2])
if err != nil {
return fmt.Errorf("Invalid VLAN ID %q: %w", entryParts[2], err)
}

if vlanID < 1 || vlanID > 4094 {
return fmt.Errorf("Invalid VLAN ID %q", entryParts[2])
}
}
}

return nil
}),
"bridge.hwaddr": validate.Optional(validate.IsNetworkMAC),
"bridge.mtu": validate.Optional(validate.IsNetworkMTU),
"bridge.driver": validate.Optional(validate.IsOneOf("native", "openvswitch")),
"bridge.external_interfaces": validate.Optional(validateExternalInterfaces),
"bridge.hwaddr": validate.Optional(validate.IsNetworkMAC),
"bridge.mtu": validate.Optional(validate.IsNetworkMTU),

"ipv4.address": validate.Optional(func(value string) error {
if validate.IsOneOf("none", "auto")(value) == nil {
Expand Down
116 changes: 113 additions & 3 deletions internal/server/network/driver_ovn.go
Original file line number Diff line number Diff line change
Expand Up @@ -350,9 +350,10 @@ func (n *ovn) getExternalSubnetInUse(uplinkNetworkName string) ([]externalSubnet
// Validate network config.
func (n *ovn) Validate(config map[string]string) error {
rules := map[string]func(value string) error{
"network": validate.IsAny,
"bridge.hwaddr": validate.Optional(validate.IsNetworkMAC),
"bridge.mtu": validate.Optional(validate.IsNetworkMTU),
"network": validate.IsAny,
"bridge.hwaddr": validate.Optional(validate.IsNetworkMAC),
"bridge.mtu": validate.Optional(validate.IsNetworkMTU),
"bridge.external_interfaces": validate.Optional(validateExternalInterfaces),
"ipv4.address": validate.Optional(func(value string) error {
if validate.IsOneOf("none", "auto")(value) == nil {
return nil
Expand Down Expand Up @@ -2352,6 +2353,115 @@ func (n *ovn) setup(update bool) error {
revert.Add(func() { _ = n.ovnnb.DeleteLogicalSwitch(context.TODO(), n.getIntSwitchName()) })
}

// Add any listed existing external interface.
if n.config["bridge.external_interfaces"] != "" {
for _, entry := range strings.Split(n.config["bridge.external_interfaces"], ",") {
entry = strings.TrimSpace(entry)

// Test for extended configuration of external interface.
entryParts := strings.Split(entry, "/")
ifParent := ""
vlanID := 0

if len(entryParts) == 3 {
vlanID, err = strconv.Atoi(entryParts[2])
if err != nil || vlanID < 1 || vlanID > 4094 {
vlanID = 0
n.logger.Warn("Ignoring invalid VLAN ID", logger.Ctx{"interface": entry, "vlanID": entryParts[2]})
} else {
entry = strings.TrimSpace(entryParts[0])
ifParent = strings.TrimSpace(entryParts[1])
}
}

iface, err := net.InterfaceByName(entry)
if err != nil {
if vlanID == 0 {
n.logger.Warn("Skipping attaching missing external interface", logger.Ctx{"interface": entry})
continue
}

// If the interface doesn't exist and VLAN ID was provided, create the missing interface.
ok, err := VLANInterfaceCreate(ifParent, entry, strconv.Itoa(vlanID), false)
if ok {
iface, err = net.InterfaceByName(entry)
}

if !ok || err != nil {
return fmt.Errorf("Failed to create external interface %q", entry)
}
} else if vlanID > 0 {
// If the interface exists and VLAN ID was provided, ensure it has the same parent and VLAN ID and is not attached to a different network.
linkInfo, err := ip.GetLinkInfoByName(entry)
if err != nil {
return fmt.Errorf("Failed to get link info for external interface %q", entry)
}

if linkInfo.Info.Kind != "vlan" || linkInfo.Link != ifParent || linkInfo.Info.Data.ID != vlanID || !(linkInfo.Master == "" || linkInfo.Master == n.name) {
return fmt.Errorf("External interface %q already in use", entry)
}
}

unused := true
addrs, err := iface.Addrs()
if err == nil {
for _, addr := range addrs {
ipAddr, _, err := net.ParseCIDR(addr.String())
if ipAddr != nil && err == nil && ipAddr.IsGlobalUnicast() {
unused = false
break
}
}
}

if !unused {
return fmt.Errorf("Only unconfigured network interfaces can be bridged")
}

lspName := networkOVN.OVNSwitchPort(fmt.Sprintf("%s-external-n%d-%s", n.getNetworkPrefix(), n.state.DB.Cluster.GetNodeID(), entry))
err = n.ovnnb.CreateLogicalSwitchPort(context.TODO(), n.getIntSwitchName(), lspName, &networkOVN.OVNSwitchPortOpts{
IPV4: "none",
IPV6: "none",
Promiscuous: true,
}, false)
if err != nil {
return fmt.Errorf("Failed to create logical switch port for %s: %w", entry, err)
}

revert.Add(func() {
_ = n.ovnnb.DeleteLogicalSwitchPort(context.TODO(), n.getIntSwitchName(), lspName)
})

// Attach host side veth interface to bridge.
integrationBridge := n.state.GlobalConfig.NetworkOVNIntegrationBridge()

vswitch, err := n.state.OVS()
if err != nil {
return fmt.Errorf("Failed to connect to OVS: %w", err)
}

err = vswitch.CreateBridgePort(context.TODO(), integrationBridge, entry, true)
if err != nil {
return err
}

revert.Add(func() { _ = vswitch.DeleteBridgePort(context.TODO(), integrationBridge, entry) })

// Link OVS port to OVN logical port.
err = vswitch.AssociateInterfaceOVNSwitchPort(context.TODO(), entry, string(lspName))
if err != nil {
return err
}

// Make sure the port is up.
link := &ip.Link{Name: entry}
err = link.SetUp()
if err != nil {
return fmt.Errorf("Failed to bring up the host interface %s: %w", entry, err)
}
}
}

// Setup IP allocation config on logical switch.
err = n.ovnnb.UpdateLogicalSwitchIPAllocation(context.TODO(), n.getIntSwitchName(), &networkOVN.OVNIPAllocationOpts{
PrefixIPv4: routerIntPortIPv4Net,
Expand Down
39 changes: 39 additions & 0 deletions internal/server/network/network_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -1442,3 +1442,42 @@ func ProxyParseAddr(data string) (*deviceConfig.ProxyAddress, error) {

return newProxyAddr, nil
}

func validateExternalInterfaces(value string) error {
for _, entry := range strings.Split(value, ",") {
entry = strings.TrimSpace(entry)

// Test for extended configuration of external interface.
entryParts := strings.Split(entry, "/")
if len(entryParts) == 3 {
// The first part is the interface name.
entry = strings.TrimSpace(entryParts[0])
}

err := validate.IsInterfaceName(entry)
if err != nil {
return fmt.Errorf("Invalid interface name %q: %w", entry, err)
}

if len(entryParts) == 3 {
// Check if the parent interface is valid.
parent := strings.TrimSpace(entryParts[1])
err := validate.IsInterfaceName(parent)
if err != nil {
return fmt.Errorf("Invalid interface name %q: %w", parent, err)
}

// Check if the VLAN ID is valid.
vlanID, err := strconv.Atoi(entryParts[2])
if err != nil {
return fmt.Errorf("Invalid VLAN ID %q: %w", entryParts[2], err)
}

if vlanID < 1 || vlanID > 4094 {
return fmt.Errorf("Invalid VLAN ID %q", entryParts[2])
}
}
}

return nil
}
1 change: 1 addition & 0 deletions internal/version/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,7 @@ var APIExtensions = []string{
"network_load_balancer_state",
"instance_nic_macvlan_mode",
"storage_lvm_cluster_create",
"network_ovn_external_interfaces",
}

// APIExtensionsCount returns the number of available API extensions.
Expand Down

0 comments on commit 596df89

Please sign in to comment.