[lxc-devel] [lxd/master] network: Adds physical network type and adds OVN support for using it as an uplink

tomponline on Github lxc-bot at linuxcontainers.org
Tue Oct 6 17:15:49 UTC 2020


A non-text attachment was scrubbed...
Name: not available
Type: text/x-mailbox
Size: 470 bytes
Desc: not available
URL: <http://lists.linuxcontainers.org/pipermail/lxc-devel/attachments/20201006/30a12609/attachment-0001.bin>
-------------- next part --------------
From efffd128341001511c5b3bbeb82af6178c71fd3d Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Tue, 6 Oct 2020 16:31:37 +0100
Subject: [PATCH 01/14] shares/validate: Whitespace

Signed-off-by: Thomas Parrott <thomas.parrott at canonical.com>
---
 shared/validate/validate.go | 1 +
 1 file changed, 1 insertion(+)

diff --git a/shared/validate/validate.go b/shared/validate/validate.go
index 356a7d8744..4217ecf89f 100644
--- a/shared/validate/validate.go
+++ b/shared/validate/validate.go
@@ -226,6 +226,7 @@ func IsNetworkAddressV4List(value string) error {
 			return err
 		}
 	}
+
 	return nil
 }
 

From 896679be6effa830345a836920db173bd3935210 Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Tue, 6 Oct 2020 16:33:00 +0100
Subject: [PATCH 02/14] lxd/network/openvswitch/ovn: Updates RecursiveDNSServer
 to be list of IPs

Signed-off-by: Thomas Parrott <thomas.parrott at canonical.com>
---
 lxd/network/openvswitch/ovn.go | 26 ++++++++++++++++++++++----
 1 file changed, 22 insertions(+), 4 deletions(-)

diff --git a/lxd/network/openvswitch/ovn.go b/lxd/network/openvswitch/ovn.go
index ef181d8706..e0a0d0dc44 100644
--- a/lxd/network/openvswitch/ovn.go
+++ b/lxd/network/openvswitch/ovn.go
@@ -66,7 +66,7 @@ type OVNDHCPv4Opts struct {
 	ServerID           net.IP
 	ServerMAC          net.HardwareAddr
 	Router             net.IP
-	RecursiveDNSServer net.IP
+	RecursiveDNSServer []net.IP
 	DomainName         string
 	LeaseTime          time.Duration
 	MTU                uint32
@@ -75,7 +75,7 @@ type OVNDHCPv4Opts struct {
 // OVNDHCPv6Opts IPv6 DHCP option set that can be created (and then applied to a switch port by resulting ID).
 type OVNDHCPv6Opts struct {
 	ServerID           net.HardwareAddr
-	RecursiveDNSServer net.IP
+	RecursiveDNSServer []net.IP
 	DNSSearchList      []string
 }
 
@@ -358,7 +358,16 @@ func (o *OVN) LogicalSwitchDHCPv4OptionsSet(switchName OVNSwitch, uuid string, s
 	}
 
 	if opts.RecursiveDNSServer != nil {
-		args = append(args, fmt.Sprintf("dns_server=%s", opts.RecursiveDNSServer.String()))
+		nsIPs := make([]string, 0, len(opts.RecursiveDNSServer))
+		for _, nsIP := range opts.RecursiveDNSServer {
+			if nsIP.To4() == nil {
+				continue // Only include IPv4 addresses.
+			}
+
+			nsIPs = append(nsIPs, nsIP.String())
+		}
+
+		args = append(args, fmt.Sprintf("dns_server={%s}", strings.Join(nsIPs, ",")))
 	}
 
 	if opts.DomainName != "" {
@@ -416,7 +425,16 @@ func (o *OVN) LogicalSwitchDHCPv6OptionsSet(switchName OVNSwitch, uuid string, s
 	}
 
 	if opts.RecursiveDNSServer != nil {
-		args = append(args, fmt.Sprintf("dns_server=%s", opts.RecursiveDNSServer.String()))
+		nsIPs := make([]string, 0, len(opts.RecursiveDNSServer))
+		for _, nsIP := range opts.RecursiveDNSServer {
+			if nsIP.To4() != nil {
+				continue // Only include IPv6 addresses.
+			}
+
+			nsIPs = append(nsIPs, nsIP.String())
+		}
+
+		args = append(args, fmt.Sprintf("dns_server={%s}", strings.Join(nsIPs, ",")))
 	}
 
 	_, err = o.nbctl(args...)

From c775eceac72df08927c5f6799e9b88244b19ea9f Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Tue, 6 Oct 2020 16:35:07 +0100
Subject: [PATCH 03/14] lxd/network/driver/ovn: Updates allocateParentPortIPs
 to detect the parent network IP address and DNS settings

Signed-off-by: Thomas Parrott <thomas.parrott at canonical.com>
---
 lxd/network/driver_ovn.go | 46 +++++++++++++++++++++++++++++++++------
 1 file changed, 39 insertions(+), 7 deletions(-)

diff --git a/lxd/network/driver_ovn.go b/lxd/network/driver_ovn.go
index 99423d4b3c..3080c9da3f 100644
--- a/lxd/network/driver_ovn.go
+++ b/lxd/network/driver_ovn.go
@@ -44,8 +44,8 @@ type ovnParentVars struct {
 	extSwitchProviderName string
 
 	// DNS.
-	dnsIPv6 net.IP
-	dnsIPv4 net.IP
+	dnsIPv6 []net.IP
+	dnsIPv4 []net.IP
 }
 
 // ovnParentPortBridgeVars parent bridge port variables used for start/stop.
@@ -391,7 +391,7 @@ func (n *ovn) setupParentPortBridge(parentNet Network, routerMAC net.HardwareAdd
 
 // allocateParentPortIPs attempts to find a free IP in the parent network's OVN ranges and then stores it in
 // ovnVolatileParentIPv4 and ovnVolatileParentIPv6 config keys on this network. Returns ovnParentVars settings.
-func (n *ovn) allocateParentPortIPs(parentNet Network, v4CIDRKey string, v6CIDRKey string, routerMAC net.HardwareAddr) (*ovnParentVars, error) {
+func (n *ovn) allocateParentPortIPs(parentNet Network, routerMAC net.HardwareAddr) (*ovnParentVars, error) {
 	v := &ovnParentVars{}
 
 	parentNetConf := parentNet.Config()
@@ -399,19 +399,51 @@ func (n *ovn) allocateParentPortIPs(parentNet Network, v4CIDRKey string, v6CIDRK
 	// Parent derived settings.
 	v.extSwitchProviderName = parentNet.Name()
 
+	// Detect parent gateway setting.
+	parentIPv4CIDR := parentNetConf["ipv4.address"]
+	if parentIPv4CIDR == "" {
+		parentIPv4CIDR = parentNetConf["ipv4.gateway"]
+	}
+
+	parentIPv6CIDR := parentNetConf["ipv6.address"]
+	if parentIPv6CIDR == "" {
+		parentIPv6CIDR = parentNetConf["ipv6.gateway"]
+	}
+
 	// Optional parent values.
-	parentIPv4, parentIPv4Net, err := net.ParseCIDR(parentNetConf[v4CIDRKey])
+	parentIPv4, parentIPv4Net, err := net.ParseCIDR(parentIPv4CIDR)
 	if err == nil {
-		v.dnsIPv4 = parentIPv4
+		v.dnsIPv4 = []net.IP{parentIPv4}
 		v.routerExtGwIPv4 = parentIPv4
 	}
 
-	parentIPv6, parentIPv6Net, err := net.ParseCIDR(parentNetConf[v6CIDRKey])
+	parentIPv6, parentIPv6Net, err := net.ParseCIDR(parentIPv6CIDR)
 	if err == nil {
-		v.dnsIPv6 = parentIPv6
+		v.dnsIPv6 = []net.IP{parentIPv6}
 		v.routerExtGwIPv6 = parentIPv6
 	}
 
+	// Detect optional DNS server list.
+	if parentNetConf["dns.nameservers"] != "" {
+		// Reset nameservers.
+		v.dnsIPv4 = nil
+		v.dnsIPv6 = nil
+
+		nsList := strings.Split(parentNetConf["dns.nameservers"], ",")
+		for _, ns := range nsList {
+			nsIP := net.ParseIP(strings.TrimSpace(ns))
+			if nsIP == nil {
+				return nil, fmt.Errorf("Invalid parent nameserver")
+			}
+
+			if nsIP.To4() == nil {
+				v.dnsIPv6 = append(v.dnsIPv6, nsIP)
+			} else {
+				v.dnsIPv4 = append(v.dnsIPv4, nsIP)
+			}
+		}
+	}
+
 	// Parse existing allocated IPs for this network on the parent network (if not set yet, will be nil).
 	routerExtPortIPv4 := net.ParseIP(n.config[ovnVolatileParentIPv4])
 	routerExtPortIPv6 := net.ParseIP(n.config[ovnVolatileParentIPv6])

From 6c99bdfd32f4cce8b6cb3032eda69113ee8b0b92 Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Tue, 6 Oct 2020 16:36:54 +0100
Subject: [PATCH 04/14] lxd/network/driver/ovn: Updates n.allocateParentPortIPs
 usage

Signed-off-by: Thomas Parrott <thomas.parrott at canonical.com>
---
 lxd/network/driver_ovn.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lxd/network/driver_ovn.go b/lxd/network/driver_ovn.go
index 3080c9da3f..4af51c3092 100644
--- a/lxd/network/driver_ovn.go
+++ b/lxd/network/driver_ovn.go
@@ -381,7 +381,7 @@ func (n *ovn) setupParentPortBridge(parentNet Network, routerMAC net.HardwareAdd
 		return nil, errors.Wrapf(err, "Network %q is not suitable for use as OVN parent", bridgeNet.name)
 	}
 
-	v, err := n.allocateParentPortIPs(parentNet, "ipv4.address", "ipv6.address", routerMAC)
+	v, err := n.allocateParentPortIPs(parentNet, routerMAC)
 	if err != nil {
 		return nil, errors.Wrapf(err, "Failed allocating parent port IPs on network %q", parentNet.Name())
 	}

From 11a0289883dbeb856344a803535cafb169e8edb3 Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Tue, 6 Oct 2020 16:37:32 +0100
Subject: [PATCH 05/14] lxd/network/driver/ovn: Updates setup IPv6 RDNSS
 setting

Signed-off-by: Thomas Parrott <thomas.parrott at canonical.com>
---
 lxd/network/driver_ovn.go | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/lxd/network/driver_ovn.go b/lxd/network/driver_ovn.go
index 4af51c3092..445e555433 100644
--- a/lxd/network/driver_ovn.go
+++ b/lxd/network/driver_ovn.go
@@ -1257,11 +1257,16 @@ func (n *ovn) setup(update bool) error {
 			adressMode = openvswitch.OVNIPv6AddressModeDHCPStateful
 		}
 
+		var recursiveDNSServer net.IP
+		if len(parent.dnsIPv6) > 0 {
+			recursiveDNSServer = parent.dnsIPv6[0] // OVN only supports 1 RA DNS server.
+		}
+
 		err = client.LogicalRouterPortSetIPv6Advertisements(n.getRouterIntPortName(), &openvswitch.OVNIPv6RAOpts{
 			AddressMode:        adressMode,
 			SendPeriodic:       true,
 			DNSSearchList:      n.getDNSSearchList(),
-			RecursiveDNSServer: parent.dnsIPv6,
+			RecursiveDNSServer: recursiveDNSServer,
 			MTU:                bridgeMTU,
 
 			// Keep these low until we support DNS search domains via DHCPv4, as otherwise RA DNSSL

From e61c4857d7cf2f753fd51880ba00a237809c4a04 Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Tue, 6 Oct 2020 16:31:26 +0100
Subject: [PATCH 06/14] shared/validate: Adds IsNetworkAddressList function

Signed-off-by: Thomas Parrott <thomas.parrott at canonical.com>
---
 shared/validate/validate.go | 13 +++++++++++++
 1 file changed, 13 insertions(+)

diff --git a/shared/validate/validate.go b/shared/validate/validate.go
index 4217ecf89f..10067d8c26 100644
--- a/shared/validate/validate.go
+++ b/shared/validate/validate.go
@@ -171,6 +171,19 @@ func IsNetworkAddress(value string) error {
 	return nil
 }
 
+// IsNetworkAddressList validates a comma delimited list of IPv4 or IPv6 addresses.
+func IsNetworkAddressList(value string) error {
+	for _, v := range strings.Split(value, ",") {
+		v = strings.TrimSpace(v)
+		err := IsNetworkAddress(v)
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
 // IsNetworkV4 validates an IPv4 CIDR string. If string is empty, returns valid.
 func IsNetworkV4(value string) error {
 	ip, subnet, err := net.ParseCIDR(value)

From c8fa53c40537fc1b39c1e1e138474166146b669f Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Tue, 6 Oct 2020 16:31:54 +0100
Subject: [PATCH 07/14] lxd/network/network/physical: Adds physical driver

Signed-off-by: Thomas Parrott <thomas.parrott at canonical.com>
---
 lxd/network/network_load.go     |   9 +-
 lxd/network/network_physical.go | 141 ++++++++++++++++++++++++++++++++
 2 files changed, 146 insertions(+), 4 deletions(-)
 create mode 100644 lxd/network/network_physical.go

diff --git a/lxd/network/network_load.go b/lxd/network/network_load.go
index cf59074663..7098181a57 100644
--- a/lxd/network/network_load.go
+++ b/lxd/network/network_load.go
@@ -5,10 +5,11 @@ import (
 )
 
 var drivers = map[string]func() Network{
-	"bridge":  func() Network { return &bridge{} },
-	"macvlan": func() Network { return &macvlan{} },
-	"sriov":   func() Network { return &sriov{} },
-	"ovn":     func() Network { return &ovn{} },
+	"bridge":   func() Network { return &bridge{} },
+	"macvlan":  func() Network { return &macvlan{} },
+	"sriov":    func() Network { return &sriov{} },
+	"ovn":      func() Network { return &ovn{} },
+	"physical": func() Network { return &physical{} },
 }
 
 // LoadByType loads a network by driver type.
diff --git a/lxd/network/network_physical.go b/lxd/network/network_physical.go
new file mode 100644
index 0000000000..2ea04f7ed4
--- /dev/null
+++ b/lxd/network/network_physical.go
@@ -0,0 +1,141 @@
+package network
+
+import (
+	"fmt"
+	"net"
+
+	"github.com/lxc/lxd/lxd/cluster"
+	"github.com/lxc/lxd/lxd/db"
+	"github.com/lxc/lxd/lxd/revert"
+	"github.com/lxc/lxd/shared/api"
+	log "github.com/lxc/lxd/shared/log15"
+	"github.com/lxc/lxd/shared/validate"
+)
+
+// physical represents a LXD physical network.
+type physical struct {
+	common
+}
+
+// Type returns the network type.
+func (n *physical) Type() string {
+	return "physical"
+}
+
+// DBType returns the network type DB ID.
+func (n *physical) DBType() db.NetworkType {
+	return db.NetworkTypePhysical
+}
+
+// Validate network config.
+func (n *physical) Validate(config map[string]string) error {
+	rules := map[string]func(value string) error{
+		"parent":           validate.Required(validate.IsNotEmpty, validInterfaceName),
+		"mtu":              validate.Optional(validate.IsNetworkMTU),
+		"vlan":             validate.Optional(validate.IsNetworkVLAN),
+		"maas.subnet.ipv4": validate.IsAny,
+		"maas.subnet.ipv6": validate.IsAny,
+		"ipv4.gateway":     validate.Optional(validate.IsNetworkAddressCIDRV4),
+		"ipv6.gateway":     validate.Optional(validate.IsNetworkAddressCIDRV6),
+		"ipv4.ovn.ranges":  validate.Optional(validate.IsNetworkRangeV4List),
+		"ipv6.ovn.ranges":  validate.Optional(validate.IsNetworkRangeV6List),
+		"dns.nameservers":  validate.Optional(validate.IsNetworkAddressList),
+	}
+
+	err := n.validate(config, rules)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// Delete deletes a network.
+func (n *physical) Delete(clientType cluster.ClientType) error {
+	n.logger.Debug("Delete", log.Ctx{"clientType": clientType})
+	return n.common.delete(clientType)
+}
+
+// Rename renames a network.
+func (n *physical) Rename(newName string) error {
+	n.logger.Debug("Rename", log.Ctx{"newName": newName})
+
+	// Rename common steps.
+	err := n.common.rename(newName)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// Start starts is a no-op.
+func (n *physical) Start() error {
+	n.logger.Debug("Start")
+
+	if n.status == api.NetworkStatusPending {
+		return fmt.Errorf("Cannot start pending network")
+	}
+
+	return nil
+}
+
+// Stop stops is a no-op.
+func (n *physical) Stop() error {
+	n.logger.Debug("Stop")
+
+	return nil
+}
+
+// Update updates the network. Accepts notification boolean indicating if this update request is coming from a
+// cluster notification, in which case do not update the database, just apply local changes needed.
+func (n *physical) Update(newNetwork api.NetworkPut, targetNode string, clientType cluster.ClientType) error {
+	n.logger.Debug("Update", log.Ctx{"clientType": clientType, "newNetwork": newNetwork})
+
+	dbUpdateNeeeded, _, oldNetwork, err := n.common.configChanged(newNetwork)
+	if err != nil {
+		return err
+	}
+
+	if !dbUpdateNeeeded {
+		return nil // Nothing changed.
+	}
+
+	revert := revert.New()
+	defer revert.Fail()
+
+	// Define a function which reverts everything.
+	revert.Add(func() {
+		// Reset changes to all nodes and database.
+		n.common.update(oldNetwork, targetNode, clientType)
+	})
+
+	// Apply changes to database.
+	err = n.common.update(newNetwork, targetNode, clientType)
+	if err != nil {
+		return err
+	}
+
+	revert.Success()
+	return nil
+}
+
+// DHCPv4Subnet returns the DHCPv4 subnet (if DHCP is enabled on network).
+func (n *physical) DHCPv4Subnet() *net.IPNet {
+	_, subnet, err := net.ParseCIDR(n.config["ipv4.gateway"])
+	if err != nil {
+		return nil
+	}
+
+	return subnet
+}
+
+// DHCPv6Subnet returns the DHCPv6 subnet (if DHCP or SLAAC is enabled on network).
+func (n *physical) DHCPv6Subnet() *net.IPNet {
+	_, subnet, err := net.ParseCIDR(n.config["ipv6.gateway"])
+	if err != nil {
+		return nil
+	}
+
+	return subnet
+}

From c6efa4013d6710a3cf83bf15e12f0e36c43cb00c Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Tue, 6 Oct 2020 16:32:25 +0100
Subject: [PATCH 08/14] lxd/db/networks: Adds physical network type constant

Signed-off-by: Thomas Parrott <thomas.parrott at canonical.com>
---
 lxd/db/networks.go | 11 +++++++----
 1 file changed, 7 insertions(+), 4 deletions(-)

diff --git a/lxd/db/networks.go b/lxd/db/networks.go
index 27eb62964d..3b10447bde 100644
--- a/lxd/db/networks.go
+++ b/lxd/db/networks.go
@@ -425,10 +425,11 @@ type NetworkType int
 
 // Network types.
 const (
-	NetworkTypeBridge  NetworkType = iota // Network type bridge.
-	NetworkTypeMacvlan                    // Network type macvlan.
-	NetworkTypeSriov                      // Network type sriov.
-	NetworkTypeOVN                        // Network type ovn.
+	NetworkTypeBridge   NetworkType = iota // Network type bridge.
+	NetworkTypeMacvlan                     // Network type macvlan.
+	NetworkTypeSriov                       // Network type sriov.
+	NetworkTypeOVN                         // Network type ovn.
+	NetworkTypePhysical                    // Network type physical.
 )
 
 // GetNetworkInAnyState returns the network with the given name.
@@ -510,6 +511,8 @@ func networkFillType(network *api.Network, netType NetworkType) {
 		network.Type = "sriov"
 	case NetworkTypeOVN:
 		network.Type = "ovn"
+	case NetworkTypePhysical:
+		network.Type = "physical"
 	default:
 		network.Type = "" // Unknown
 	}

From 5578e5c1a5aea1d218de1af55b9b26755e91ce54 Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Tue, 6 Oct 2020 17:24:51 +0100
Subject: [PATCH 09/14] api: Adds network_type_physical extension

Signed-off-by: Thomas Parrott <thomas.parrott at canonical.com>
---
 doc/api-extensions.md | 7 ++++++-
 shared/version/api.go | 1 +
 2 files changed, 7 insertions(+), 1 deletion(-)

diff --git a/doc/api-extensions.md b/doc/api-extensions.md
index d7c233c3e6..bc3ee9e2d2 100644
--- a/doc/api-extensions.md
+++ b/doc/api-extensions.md
@@ -1185,4 +1185,9 @@ when restoring a custom volume backup.
 
 ## storage\_rsync\_compression
 Adds `rsync.compression` config key to storage pools. This key can be used
-to disable compression in rsync while migrating storage pools.
\ No newline at end of file
+to disable compression in rsync while migrating storage pools.
+
+## network\_type\_physical
+Adds support for additional network type `physical` that can be used as an uplink for `ovn` networks.
+
+The interface specified by `parent` on the `physical` network will be connected to the `ovn` network's gateway.
diff --git a/shared/version/api.go b/shared/version/api.go
index ba9380b676..d5035f7161 100644
--- a/shared/version/api.go
+++ b/shared/version/api.go
@@ -229,6 +229,7 @@ var APIExtensions = []string{
 	"custom_volume_backup",
 	"backup_override_name",
 	"storage_rsync_compression",
+	"network_type_physical",
 }
 
 // APIExtensionsCount returns the number of available API extensions.

From d2fee1c90a17d64859e33363645420c219eb4421 Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Tue, 6 Oct 2020 18:09:24 +0100
Subject: [PATCH 10/14] lxd/network/network/utils: Adds VLANInterfaceCreate
 function

Signed-off-by: Thomas Parrott <thomas.parrott at canonical.com>
---
 lxd/network/network_utils.go | 31 +++++++++++++++++++++++++++++++
 1 file changed, 31 insertions(+)

diff --git a/lxd/network/network_utils.go b/lxd/network/network_utils.go
index 010a00978c..c9b97be607 100644
--- a/lxd/network/network_utils.go
+++ b/lxd/network/network_utils.go
@@ -26,6 +26,7 @@ import (
 	"github.com/lxc/lxd/lxd/instance/instancetype"
 	"github.com/lxc/lxd/lxd/project"
 	"github.com/lxc/lxd/lxd/state"
+	"github.com/lxc/lxd/lxd/util"
 	"github.com/lxc/lxd/shared"
 	"github.com/lxc/lxd/shared/api"
 	"github.com/lxc/lxd/shared/logger"
@@ -1014,3 +1015,33 @@ func parseIPRanges(ipRangesList string, allowedNets ...*net.IPNet) ([]*shared.IP
 
 	return netIPRanges, nil
 }
+
+// VLANInterfaceCreate creates a VLAN interface on parent interface (if needed).
+// Returns boolean indicating if VLAN interface was created.
+func VLANInterfaceCreate(parent string, vlanDevice string, vlanID string) (bool, error) {
+	if vlanID == "" {
+		return false, nil
+	}
+
+	if shared.PathExists(fmt.Sprintf("/sys/class/net/%s", vlanDevice)) {
+		return false, nil
+	}
+
+	// Bring the parent interface up so we can add a vlan to it.
+	_, err := shared.RunCommand("ip", "link", "set", "dev", parent, "up")
+	if err != nil {
+		return false, errors.Wrapf(err, "Failed to bring up parent %q", parent)
+	}
+
+	// Add VLAN interface on top of parent.
+	_, err = shared.RunCommand("ip", "link", "add", "link", parent, "name", vlanDevice, "up", "type", "vlan", "id", vlanID)
+	if err != nil {
+		return false, errors.Wrapf(err, "Failed to create VLAN interface %q on %q", vlanDevice, parent)
+	}
+
+	// Attempt to disable IPv6 router advertisement acceptance.
+	util.SysctlSet(fmt.Sprintf("net/ipv6/conf/%s/accept_ra", vlanDevice), "0")
+
+	// We created a new vlan interface, return true.
+	return true, nil
+}

From 1c12992a05b67cd70ab44b2072cb8c30df2ffaf9 Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Tue, 6 Oct 2020 18:09:42 +0100
Subject: [PATCH 11/14] lxd/device/device/utils/network:
 network.VLANInterfaceCreate usage

Signed-off-by: Thomas Parrott <thomas.parrott at canonical.com>
---
 lxd/device/device_utils_network.go | 22 +++++-----------------
 1 file changed, 5 insertions(+), 17 deletions(-)

diff --git a/lxd/device/device_utils_network.go b/lxd/device/device_utils_network.go
index eb95d3501b..6db9656885 100644
--- a/lxd/device/device_utils_network.go
+++ b/lxd/device/device_utils_network.go
@@ -18,7 +18,6 @@ import (
 	"github.com/lxc/lxd/lxd/project"
 	"github.com/lxc/lxd/lxd/revert"
 	"github.com/lxc/lxd/lxd/state"
-	"github.com/lxc/lxd/lxd/util"
 	"github.com/lxc/lxd/shared"
 	"github.com/lxc/lxd/shared/logger"
 	"github.com/lxc/lxd/shared/units"
@@ -111,23 +110,12 @@ func networkRemoveInterfaceIfNeeded(state *state.State, nic string, current inst
 // networkCreateVlanDeviceIfNeeded creates a VLAN device if doesn't already exist.
 func networkCreateVlanDeviceIfNeeded(state *state.State, parent string, vlanDevice string, vlanID string) (string, error) {
 	if vlanID != "" {
-		if !shared.PathExists(fmt.Sprintf("/sys/class/net/%s", vlanDevice)) {
-			// Bring the parent interface up so we can add a vlan to it.
-			_, err := shared.RunCommand("ip", "link", "set", "dev", parent, "up")
-			if err != nil {
-				return "", fmt.Errorf("Failed to bring up parent %s: %v", parent, err)
-			}
-
-			// Add VLAN interface on top of parent.
-			_, err = shared.RunCommand("ip", "link", "add", "link", parent, "name", vlanDevice, "up", "type", "vlan", "id", vlanID)
-			if err != nil {
-				return "", err
-			}
-
-			// Attempt to disable IPv6 router advertisement acceptance.
-			util.SysctlSet(fmt.Sprintf("net/ipv6/conf/%s/accept_ra", vlanDevice), "0")
+		created, err := network.VLANInterfaceCreate(parent, vlanDevice, vlanID)
+		if err != nil {
+			return "", err
+		}
 
-			// We created a new vlan interface, return true.
+		if created {
 			return "created", nil
 		}
 

From f229e6614a3d544a5798c58a1957efa2199c3669 Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Tue, 6 Oct 2020 17:31:24 +0100
Subject: [PATCH 12/14] doc/networks: Clarifies use of ovn ranges settings in
 bridge network

Signed-off-by: Thomas Parrott <thomas.parrott at canonical.com>
---
 doc/networks.md | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/doc/networks.md b/doc/networks.md
index 47d3386963..a9d3cb6c25 100644
--- a/doc/networks.md
+++ b/doc/networks.md
@@ -83,7 +83,7 @@ ipv4.firewall                   | boolean   | ipv4 address          | true
 ipv4.nat.address                | string    | ipv4 address          | -                         | The source address used for outbound traffic from the bridge
 ipv4.nat                        | boolean   | ipv4 address          | false                     | Whether to NAT (will default to true if unset and a random ipv4.address is generated)
 ipv4.nat.order                  | string    | ipv4 address          | before                    | Whether to add the required NAT rules before or after any pre-existing rules
-ipv4.ovn.ranges                 | string    | -                     | none                      | Comma separate list of IPv4 ranges to use for child OVN networks (FIRST-LAST format)
+ipv4.ovn.ranges                 | string    | -                     | none                      | Comma separate list of IPv4 ranges to use for child OVN network routers (FIRST-LAST format)
 ipv4.routes                     | string    | ipv4 address          | -                         | Comma separated list of additional IPv4 CIDR subnets to route to the bridge
 ipv4.routing                    | boolean   | ipv4 address          | true                      | Whether to route traffic in and out of the bridge
 ipv6.address                    | string    | standard mode         | random unused subnet      | IPv6 address for the bridge (CIDR notation). Use "none" to turn off IPv6 or "auto" to generate a new one
@@ -95,7 +95,7 @@ ipv6.firewall                   | boolean   | ipv6 address          | true
 ipv6.nat.address                | string    | ipv6 address          | -                         | The source address used for outbound traffic from the bridge
 ipv6.nat                        | boolean   | ipv6 address          | false                     | Whether to NAT (will default to true if unset and a random ipv6.address is generated)
 ipv6.nat.order                  | string    | ipv6 address          | before                    | Whether to add the required NAT rules before or after any pre-existing rules
-ipv6.ovn.ranges                 | string    | -                     | none                      | Comma separate list of IPv6 ranges to use for child OVN networks (FIRST-LAST format)
+ipv6.ovn.ranges                 | string    | -                     | none                      | Comma separate list of IPv6 ranges to use for child OVN network routers (FIRST-LAST format)
 ipv6.routes                     | string    | ipv6 address          | -                         | Comma separated list of additional IPv6 CIDR subnets to route to the bridge
 ipv6.routing                    | boolean   | ipv6 address          | true                      | Whether to route traffic in and out of the bridge
 maas.subnet.ipv4                | string    | ipv4 address          | -                         | MAAS IPv4 subnet to register instances in (when using `network` property on nic)

From 08abb33dfaaa4971d59d6e435cc69ec722c05699 Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Tue, 6 Oct 2020 17:31:45 +0100
Subject: [PATCH 13/14] doc/networks: Adds docs for physical network type

Signed-off-by: Thomas Parrott <thomas.parrott at canonical.com>
---
 doc/networks.md | 20 ++++++++++++++++++++
 1 file changed, 20 insertions(+)

diff --git a/doc/networks.md b/doc/networks.md
index a9d3cb6c25..6df04b13c4 100644
--- a/doc/networks.md
+++ b/doc/networks.md
@@ -6,6 +6,7 @@ LXD supports the following network types:
  - [macvlan](#network-macvlan): Provides preset configuration to use when connecting instances to a parent macvlan interface.
  - [sriov](#network-sriov): Provides preset configuration to use when connecting instances to a parent SR-IOV interface.
  - [ovn](#network-ovn): Creates a logical network using the OVN software defined networking system.
+ - [physical](#network-physical): Provides preset configuration to use when connecting OVN networks to a parent interface.
 
 The desired type can be specified using the `--type` argument, e.g.
 
@@ -299,3 +300,22 @@ ipv4.address                    | string    | standard mode         | random unu
 ipv6.address                    | string    | standard mode         | random unused subnet      | IPv6 address for the bridge (CIDR notation). Use "none" to turn off IPv6 or "auto" to generate a new one
 ipv6.dhcp.stateful              | boolean   | ipv6 dhcp             | false                     | Whether to allocate addresses using DHCP
 network                         | string    | -                     | -                         | Parent network to use for outbound external network access
+
+## network: physical
+
+The physical network type allows one to specify presets to use when connecting OVN networks to a parent interface.
+
+Network configuration properties:
+
+Key                             | Type      | Condition             | Default                   | Description
+:--                             | :--       | :--                   | :--                       | :--
+maas.subnet.ipv4                | string    | ipv4 address          | -                         | MAAS IPv4 subnet to register instances in (when using `network` property on nic)
+maas.subnet.ipv6                | string    | ipv6 address          | -                         | MAAS IPv6 subnet to register instances in (when using `network` property on nic)
+mtu                             | integer   | -                     | -                         | The MTU of the new interface
+parent                          | string    | -                     | -                         | Parent interface to create sriov NICs on
+vlan                            | integer   | -                     | -                         | The VLAN ID to attach to
+ipv4.gateway                    | string    | standard mode         | -                         | IPv4 address for the gateway and network (CIDR notation)
+ipv4.ovn.ranges                 | string    | -                     | none                      | Comma separate list of IPv4 ranges to use for child OVN network routers (FIRST-LAST format)
+ipv6.gateway                    | string    | standard mode         | -                         | IPv6 address for the gateway and network  (CIDR notation)
+ipv6.ovn.ranges                 | string    | -                     | none                      | Comma separate list of IPv6 ranges to use for child OVN network routers (FIRST-LAST format)
+dns.nameservers                 | string    | standard mode         | -                         | List of DNS server IPs on physical network

From 6a377d286621cd0682ee5d9fcc3b2e3556bab5bf Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Tue, 6 Oct 2020 16:50:15 +0100
Subject: [PATCH 14/14] lxd/network/driver/ovn: Adds support for physical
 network as uplink

Signed-off-by: Thomas Parrott <thomas.parrott at canonical.com>
---
 lxd/network/driver_ovn.go | 115 ++++++++++++++++++++++++++++++++++++++
 1 file changed, 115 insertions(+)

diff --git a/lxd/network/driver_ovn.go b/lxd/network/driver_ovn.go
index 445e555433..c748c0d734 100644
--- a/lxd/network/driver_ovn.go
+++ b/lxd/network/driver_ovn.go
@@ -363,6 +363,8 @@ func (n *ovn) setupParentPort(routerMAC net.HardwareAddr) (*ovnParentVars, error
 	switch parentNet.Type() {
 	case "bridge":
 		return n.setupParentPortBridge(parentNet, routerMAC)
+	case "physical":
+		return n.setupParentPortPhysical(parentNet, routerMAC)
 	}
 
 	return nil, fmt.Errorf("Failed setting up parent port, network type %q unsupported as OVN parent", parentNet.Type())
@@ -389,6 +391,17 @@ func (n *ovn) setupParentPortBridge(parentNet Network, routerMAC net.HardwareAdd
 	return v, nil
 }
 
+// setupParentPortPhysical allocates external IPs on the parent network.
+// Returns the derived ovnParentVars settings.
+func (n *ovn) setupParentPortPhysical(parentNet Network, routerMAC net.HardwareAddr) (*ovnParentVars, error) {
+	v, err := n.allocateParentPortIPs(parentNet, routerMAC)
+	if err != nil {
+		return nil, errors.Wrapf(err, "Failed allocating parent port IPs on network %q", parentNet.Name())
+	}
+
+	return v, nil
+}
+
 // allocateParentPortIPs attempts to find a free IP in the parent network's OVN ranges and then stores it in
 // ovnVolatileParentIPv4 and ovnVolatileParentIPv6 config keys on this network. Returns ovnParentVars settings.
 func (n *ovn) allocateParentPortIPs(parentNet Network, routerMAC net.HardwareAddr) (*ovnParentVars, error) {
@@ -632,6 +645,8 @@ func (n *ovn) startParentPort() error {
 	switch parentNet.Type() {
 	case "bridge":
 		return n.startParentPortBridge(parentNet)
+	case "physical":
+		return n.startParentPortPhysical(parentNet)
 	}
 
 	return fmt.Errorf("Failed starting parent port, network type %q unsupported as OVN parent", parentNet.Type())
@@ -745,6 +760,59 @@ func (n *ovn) startParentPortBridge(parentNet Network) error {
 	return nil
 }
 
+// startParentPortPhysical creates OVS bridge (if doesn't exist) and connects parent interface to the OVS bridge.
+func (n *ovn) startParentPortPhysical(parentNet Network) error {
+	vars := n.parentPortBridgeVars(parentNet)
+
+	// Do this after gaining lock so that on failure we revert before release locking.
+	revert := revert.New()
+	defer revert.Fail()
+
+	parentConfig := parentNet.Config()
+	uplinkDev := GetHostDevice(parentConfig["parent"], parentConfig["vlan"])
+
+	_, err := VLANInterfaceCreate(parentConfig["parent"], uplinkDev, parentConfig["vlan"])
+	if err != nil {
+		return err
+	}
+
+	// Ensure correct sysctls are set on uplink interface to avoid getting IPv6 link-local addresses.
+	_, err = shared.RunCommand("sysctl",
+		fmt.Sprintf("net/ipv6/conf/%s/disable_ipv6=1", uplinkDev),
+		fmt.Sprintf("net/ipv6/conf/%s/forwarding=0", uplinkDev),
+	)
+	if err != nil {
+		return errors.Wrapf(err, "Failed to configure uplink interface %q", uplinkDev)
+	}
+
+	// Create parent OVS bridge if needed.
+	ovs := openvswitch.NewOVS()
+	err = ovs.BridgeAdd(vars.ovsBridge, true)
+	if err != nil {
+		return errors.Wrapf(err, "Failed to create parent uplink OVS bridge %q", vars.ovsBridge)
+	}
+
+	// Connect OVS end veth interface to OVS bridge.
+	err = ovs.BridgePortAdd(vars.ovsBridge, uplinkDev, true)
+	if err != nil {
+		return errors.Wrapf(err, "Failed to connect uplink VF interface %q to parent OVS bridge %q", uplinkDev, vars.ovsBridge)
+	}
+
+	// Associate OVS bridge to logical OVN provider.
+	err = ovs.OVNBridgeMappingAdd(vars.ovsBridge, parentNet.Name())
+	if err != nil {
+		return errors.Wrapf(err, "Failed to associate parent OVS bridge %q to OVN provider %q", vars.ovsBridge, parentNet.Name())
+	}
+
+	_, err = shared.RunCommand("ip", "link", "set", uplinkDev, "up")
+	if err != nil {
+		return errors.Wrapf(err, "Failed to bring up parent interface %q", uplinkDev)
+	}
+
+	revert.Success()
+	return nil
+}
+
 // deleteParentPort deletes the parent uplink connection.
 func (n *ovn) deleteParentPort() error {
 	// Parent network must be in default project.
@@ -761,6 +829,8 @@ func (n *ovn) deleteParentPort() error {
 		switch parentNet.Type() {
 		case "bridge":
 			return n.deleteParentPortBridge(parentNet)
+		case "physical":
+			return n.deleteParentPortPhysical(parentNet)
 		}
 
 		return fmt.Errorf("Failed deleting parent port, network type %q unsupported as OVN parent", parentNet.Type())
@@ -819,6 +889,51 @@ func (n *ovn) deleteParentPortBridge(parentNet Network) error {
 	return nil
 }
 
+// deleteParentPortPhysical deletes parent uplink OVS bridge, OVN bridge mappings if not in use.
+func (n *ovn) deleteParentPortPhysical(parentNet Network) error {
+	// Check OVS uplink bridge exists, if it does, check how many ports it has.
+	releaseIF := false
+	vars := n.parentPortBridgeVars(parentNet)
+	if shared.PathExists(fmt.Sprintf("/sys/class/net/%s", vars.ovsBridge)) {
+		ovs := openvswitch.NewOVS()
+		ports, err := ovs.BridgePortList(vars.ovsBridge)
+		if err != nil {
+			return err
+		}
+
+		// If the OVS bridge has only 1 port (the parent interface) or fewer connected then delete it.
+		if len(ports) <= 1 {
+			releaseIF = true
+
+			err = ovs.OVNBridgeMappingDelete(vars.ovsBridge, parentNet.Name())
+			if err != nil {
+				return err
+			}
+
+			err = ovs.BridgeDelete(vars.ovsBridge)
+			if err != nil {
+				return err
+			}
+		}
+	} else {
+		releaseIF = true // Remove the veths if OVS bridge already gone.
+	}
+
+	// Remove the veth interfaces if they exist.
+	if releaseIF {
+		parentConfig := parentNet.Config()
+		parentDev := GetHostDevice(parentConfig["parent"], parentConfig["vlan"])
+		if parentDev != "" && shared.PathExists(fmt.Sprintf("/sys/class/net/%s", parentDev)) {
+			_, err := shared.RunCommand("ip", "link", "set", parentDev, "down")
+			if err != nil {
+				return errors.Wrapf(err, "Failed to bring down parent interface %q", parentDev)
+			}
+		}
+	}
+
+	return nil
+}
+
 // FillConfig fills requested config with any default values.
 func (n *ovn) FillConfig(config map[string]string) error {
 	if config["ipv4.address"] == "" {


More information about the lxc-devel mailing list