[lxc-devel] [lxd/master] Validate: Makes shared/validate helpers non-optional

tomponline on Github lxc-bot at linuxcontainers.org
Mon Aug 3 13:13:57 UTC 2020


A non-text attachment was scrubbed...
Name: not available
Type: text/x-mailbox
Size: 491 bytes
Desc: not available
URL: <http://lists.linuxcontainers.org/pipermail/lxc-devel/attachments/20200803/e9629b0d/attachment-0001.bin>
-------------- next part --------------
From a573f047f39f03f45a8a1af62716cf69e3e18c35 Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Mon, 3 Aug 2020 11:48:56 +0100
Subject: [PATCH 01/33] shared/validate: Makes IsUint32 non-optional

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

diff --git a/shared/validate/validate.go b/shared/validate/validate.go
index 586f0c9e35..24da1237db 100644
--- a/shared/validate/validate.go
+++ b/shared/validate/validate.go
@@ -57,10 +57,6 @@ func IsUint8(value string) error {
 
 // IsUint32 validates whether the string can be converted to an uint32.
 func IsUint32(value string) error {
-	if value == "" {
-		return nil
-	}
-
 	_, err := strconv.ParseUint(value, 10, 32)
 	if err != nil {
 		return fmt.Errorf("Invalid value for uint32 %q: %v", value, err)

From faa11bdadb31ce900e453a2a6fb55230584ac88f Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Mon, 3 Aug 2020 11:49:19 +0100
Subject: [PATCH 02/33] lxd: Wraps validate.IsUint32 in validate.Optional

Due to validate.IsUint32 now being non-optional.

Signed-off-by: Thomas Parrott <thomas.parrott at canonical.com>
---
 lxd/api_project.go                        | 8 ++++----
 lxd/device/disk.go                        | 2 +-
 lxd/device/nic.go                         | 6 +++---
 lxd/storage/drivers/driver_lvm.go         | 2 +-
 lxd/storage/drivers/driver_lvm_volumes.go | 2 +-
 lxd/storage_pools_config.go               | 2 +-
 6 files changed, 11 insertions(+), 11 deletions(-)

diff --git a/lxd/api_project.go b/lxd/api_project.go
index 82e757fbb8..01e2256aeb 100644
--- a/lxd/api_project.go
+++ b/lxd/api_project.go
@@ -526,11 +526,11 @@ var projectConfigKeys = map[string]func(value string) error{
 	"features.profiles":              validate.IsBool,
 	"features.images":                validate.IsBool,
 	"features.storage.volumes":       validate.IsBool,
-	"limits.containers":              validate.IsUint32,
-	"limits.virtual-machines":        validate.IsUint32,
+	"limits.containers":              validate.Optional(validate.IsUint32),
+	"limits.virtual-machines":        validate.Optional(validate.IsUint32),
 	"limits.memory":                  validate.IsSize,
-	"limits.processes":               validate.IsUint32,
-	"limits.cpu":                     validate.IsUint32,
+	"limits.processes":               validate.Optional(validate.IsUint32),
+	"limits.cpu":                     validate.Optional(validate.IsUint32),
 	"limits.disk":                    validate.IsSize,
 	"restricted":                     validate.IsBool,
 	"restricted.containers.nesting":  isEitherAllowOrBlock,
diff --git a/lxd/device/disk.go b/lxd/device/disk.go
index 6c3f7a144f..051753f57f 100644
--- a/lxd/device/disk.go
+++ b/lxd/device/disk.go
@@ -90,7 +90,7 @@ func (d *disk) validateConfig(instConf instance.ConfigReader) error {
 		"raw.mount.options": validate.IsAny,
 		"ceph.cluster_name": validate.IsAny,
 		"ceph.user_name":    validate.IsAny,
-		"boot.priority":     validate.IsUint32,
+		"boot.priority":     validate.Optional(validate.IsUint32),
 		"path":              validate.IsAny,
 	}
 
diff --git a/lxd/device/nic.go b/lxd/device/nic.go
index 09ec02cf23..6e933bc443 100644
--- a/lxd/device/nic.go
+++ b/lxd/device/nic.go
@@ -27,13 +27,13 @@ func nicValidationRules(requiredFields []string, optionalFields []string) map[st
 		"ipv6.address":            validate.IsNetworkAddressV6,
 		"ipv4.routes":             validate.IsNetworkV4List,
 		"ipv6.routes":             validate.IsNetworkV6List,
-		"boot.priority":           validate.IsUint32,
+		"boot.priority":           validate.Optional(validate.IsUint32),
 		"ipv4.gateway":            networkValidGateway,
 		"ipv6.gateway":            networkValidGateway,
 		"ipv4.host_address":       validate.IsNetworkAddressV4,
 		"ipv6.host_address":       validate.IsNetworkAddressV6,
-		"ipv4.host_table":         validate.IsUint32,
-		"ipv6.host_table":         validate.IsUint32,
+		"ipv4.host_table":         validate.Optional(validate.IsUint32),
+		"ipv6.host_table":         validate.Optional(validate.IsUint32),
 	}
 
 	validators := map[string]func(value string) error{}
diff --git a/lxd/storage/drivers/driver_lvm.go b/lxd/storage/drivers/driver_lvm.go
index 598ea3fe7d..8798a3076e 100644
--- a/lxd/storage/drivers/driver_lvm.go
+++ b/lxd/storage/drivers/driver_lvm.go
@@ -439,7 +439,7 @@ func (d *lvm) Validate(config map[string]string) error {
 			}
 			return validate.IsOneOf(value, lvmAllowedFilesystems)
 		},
-		"volume.lvm.stripes":      validate.IsUint32,
+		"volume.lvm.stripes":      validate.Optional(validate.IsUint32),
 		"volume.lvm.stripes.size": validate.IsSize,
 		"lvm.vg.force_reuse":      validate.IsBool,
 	}
diff --git a/lxd/storage/drivers/driver_lvm_volumes.go b/lxd/storage/drivers/driver_lvm_volumes.go
index 2ea0c39022..2d9c730076 100644
--- a/lxd/storage/drivers/driver_lvm_volumes.go
+++ b/lxd/storage/drivers/driver_lvm_volumes.go
@@ -248,7 +248,7 @@ func (d *lvm) ValidateVolume(vol Volume, removeUnknownKeys bool) error {
 			}
 			return validate.IsOneOf(value, lvmAllowedFilesystems)
 		},
-		"lvm.stripes":      validate.IsUint32,
+		"lvm.stripes":      validate.Optional(validate.IsUint32),
 		"lvm.stripes.size": validate.IsSize,
 	}
 
diff --git a/lxd/storage_pools_config.go b/lxd/storage_pools_config.go
index 1e0d5fef83..0dc57cf710 100644
--- a/lxd/storage_pools_config.go
+++ b/lxd/storage_pools_config.go
@@ -46,7 +46,7 @@ var storagePoolConfigKeys = map[string]func(value string) error{
 	"lvm.thinpool_name":       validate.IsAny,
 	"lvm.use_thinpool":        validate.IsBool,
 	"lvm.vg_name":             validate.IsAny,
-	"volume.lvm.stripes":      validate.IsUint32,
+	"volume.lvm.stripes":      validate.Optional(validate.IsUint32),
 	"volume.lvm.stripes.size": validate.IsSize,
 	"lvm.vg.force_reuse":      validate.IsBool,
 

From 5b1320ad82804759b223c20ab45a31dbe4ba9c86 Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Mon, 3 Aug 2020 11:49:54 +0100
Subject: [PATCH 03/33] shared/instance: Wraps validate.IsUint32 in
 validate.Optional

Due to validate.IsUint32 now being non-optional.

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

diff --git a/shared/instance.go b/shared/instance.go
index 9ba5760dcf..a565a3cbab 100644
--- a/shared/instance.go
+++ b/shared/instance.go
@@ -177,8 +177,8 @@ var KnownInstanceConfigKeys = map[string]func(value string) error{
 	"linux.kernel_modules": validate.IsAny,
 
 	"migration.incremental.memory":            validate.IsBool,
-	"migration.incremental.memory.iterations": validate.IsUint32,
-	"migration.incremental.memory.goal":       validate.IsUint32,
+	"migration.incremental.memory.iterations": validate.Optional(validate.IsUint32),
+	"migration.incremental.memory.goal":       validate.Optional(validate.IsUint32),
 
 	"nvidia.runtime":             validate.IsBool,
 	"nvidia.driver.capabilities": validate.IsAny,
@@ -193,9 +193,9 @@ var KnownInstanceConfigKeys = map[string]func(value string) error{
 	"security.protection.delete": validate.IsBool,
 	"security.protection.shift":  validate.IsBool,
 
-	"security.idmap.base":     validate.IsUint32,
+	"security.idmap.base":     validate.Optional(validate.IsUint32),
 	"security.idmap.isolated": validate.IsBool,
-	"security.idmap.size":     validate.IsUint32,
+	"security.idmap.size":     validate.Optional(validate.IsUint32),
 
 	"security.secureboot": validate.IsBool,
 

From f6bbb88c5ce216d22dba32e01996be10169b0171 Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Mon, 3 Aug 2020 11:50:44 +0100
Subject: [PATCH 04/33] shared/validate: Makes IsUint8 non-optional

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

diff --git a/shared/validate/validate.go b/shared/validate/validate.go
index 24da1237db..49d944fef1 100644
--- a/shared/validate/validate.go
+++ b/shared/validate/validate.go
@@ -43,10 +43,6 @@ func IsInt64(value string) error {
 
 // IsUint8 validates whether the string can be converted to an uint8.
 func IsUint8(value string) error {
-	if value == "" {
-		return nil
-	}
-
 	_, err := strconv.ParseUint(value, 10, 8)
 	if err != nil {
 		return fmt.Errorf("Invalid value for an integer %q. Must be between 0 and 255", value)

From a96a34a715b27c69df173ac2d3738c81d75dbea5 Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Mon, 3 Aug 2020 11:51:22 +0100
Subject: [PATCH 05/33] lxd/network/driver/bridge: Wraps validate.IsUint8 in
 validate.Optional

Due to validate.IsUint32 now being non-optional.

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

diff --git a/lxd/network/driver_bridge.go b/lxd/network/driver_bridge.go
index d0afd95f55..6199ed65fe 100644
--- a/lxd/network/driver_bridge.go
+++ b/lxd/network/driver_bridge.go
@@ -257,7 +257,7 @@ func (n *bridge) Validate(config map[string]string) error {
 			case "inteface":
 				rules[k] = ValidNetworkName
 			case "ttl":
-				rules[k] = validate.IsUint8
+				rules[k] = validate.Optional(validate.IsUint8)
 			}
 		}
 	}

From 0a2316a0b4d3d8530504f87d3714278efa96e28f Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Mon, 3 Aug 2020 11:52:26 +0100
Subject: [PATCH 06/33] shared/validate: Makes IsPriority non-optional

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

diff --git a/shared/validate/validate.go b/shared/validate/validate.go
index 49d944fef1..08fab2c1ff 100644
--- a/shared/validate/validate.go
+++ b/shared/validate/validate.go
@@ -63,10 +63,6 @@ func IsUint32(value string) error {
 
 // IsPriority validates priority number.
 func IsPriority(value string) error {
-	if value == "" {
-		return nil
-	}
-
 	valueInt, err := strconv.ParseInt(value, 10, 64)
 	if err != nil {
 		return fmt.Errorf("Invalid value for an integer %q", value)

From de8ae02f9189b234816aa98d6e08d33f07853f14 Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Mon, 3 Aug 2020 11:53:20 +0100
Subject: [PATCH 07/33] shared/instance: Wraps validate.IsPriority in
 validate.Optional

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

diff --git a/shared/instance.go b/shared/instance.go
index a565a3cbab..306bd58fac 100644
--- a/shared/instance.go
+++ b/shared/instance.go
@@ -133,9 +133,9 @@ var KnownInstanceConfigKeys = map[string]func(value string) error{
 
 		return nil
 	},
-	"limits.cpu.priority": validate.IsPriority,
+	"limits.cpu.priority": validate.Optional(validate.IsPriority),
 
-	"limits.disk.priority": validate.IsPriority,
+	"limits.disk.priority": validate.Optional(validate.IsPriority),
 
 	"limits.hugepages.64KB": validate.IsSize,
 	"limits.hugepages.1MB":  validate.IsSize,
@@ -167,10 +167,10 @@ var KnownInstanceConfigKeys = map[string]func(value string) error{
 		return validate.IsOneOf(value, []string{"soft", "hard"})
 	},
 	"limits.memory.swap":          validate.IsBool,
-	"limits.memory.swap.priority": validate.IsPriority,
+	"limits.memory.swap.priority": validate.Optional(validate.IsPriority),
 	"limits.memory.hugepages":     validate.IsBool,
 
-	"limits.network.priority": validate.IsPriority,
+	"limits.network.priority": validate.Optional(validate.IsPriority),
 
 	"limits.processes": validate.Optional(validate.IsInt64),
 

From 52ce0d90f816f0bee4b99ed993b9dc4e1772b60c Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Mon, 3 Aug 2020 11:54:06 +0100
Subject: [PATCH 08/33] shared/validate: Makes IsBool non-optional

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

diff --git a/shared/validate/validate.go b/shared/validate/validate.go
index 08fab2c1ff..cacdb38ab1 100644
--- a/shared/validate/validate.go
+++ b/shared/validate/validate.go
@@ -77,10 +77,6 @@ func IsPriority(value string) error {
 
 // IsBool validates if string can be understood as a bool.
 func IsBool(value string) error {
-	if value == "" {
-		return nil
-	}
-
 	if !stringInSlice(strings.ToLower(value), []string{"true", "false", "yes", "no", "1", "0", "on", "off"}) {
 		return fmt.Errorf("Invalid value for a boolean %q", value)
 	}

From c7b46cd66149b0cb4d8e6091fff8bcb3b33eea7b Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Mon, 3 Aug 2020 11:55:31 +0100
Subject: [PATCH 09/33] lxd: Wraps validate.IsBool in validate.Optional

Due to IsBool being required now.

Signed-off-by: Thomas Parrott <thomas.parrott at canonical.com>
---
 lxd/api_project.go                        |  8 ++++----
 lxd/device/disk.go                        | 10 +++++-----
 lxd/device/proxy.go                       |  4 ++--
 lxd/device/unix_common.go                 |  2 +-
 lxd/device/unix_hotplug.go                |  2 +-
 lxd/device/usb.go                         |  2 +-
 lxd/network/driver_bridge.go              | 18 +++++++++---------
 lxd/storage/drivers/driver_ceph.go        |  4 ++--
 lxd/storage/drivers/driver_lvm.go         |  4 ++--
 lxd/storage/drivers/driver_zfs.go         |  6 +++---
 lxd/storage/drivers/driver_zfs_volumes.go |  4 ++--
 lxd/storage/utils.go                      | 12 ++++++------
 lxd/storage_pools_config.go               | 14 +++++++-------
 13 files changed, 45 insertions(+), 45 deletions(-)

diff --git a/lxd/api_project.go b/lxd/api_project.go
index 01e2256aeb..c4541ac8c4 100644
--- a/lxd/api_project.go
+++ b/lxd/api_project.go
@@ -523,16 +523,16 @@ func isEitherAllowOrBlockOrManaged(value string) error {
 
 // Validate the project configuration
 var projectConfigKeys = map[string]func(value string) error{
-	"features.profiles":              validate.IsBool,
-	"features.images":                validate.IsBool,
-	"features.storage.volumes":       validate.IsBool,
+	"features.profiles":              validate.Optional(validate.IsBool),
+	"features.images":                validate.Optional(validate.IsBool),
+	"features.storage.volumes":       validate.Optional(validate.IsBool),
 	"limits.containers":              validate.Optional(validate.IsUint32),
 	"limits.virtual-machines":        validate.Optional(validate.IsUint32),
 	"limits.memory":                  validate.IsSize,
 	"limits.processes":               validate.Optional(validate.IsUint32),
 	"limits.cpu":                     validate.Optional(validate.IsUint32),
 	"limits.disk":                    validate.IsSize,
-	"restricted":                     validate.IsBool,
+	"restricted":                     validate.Optional(validate.IsBool),
 	"restricted.containers.nesting":  isEitherAllowOrBlock,
 	"restricted.containers.lowlevel": isEitherAllowOrBlock,
 	"restricted.containers.privilege": func(value string) error {
diff --git a/lxd/device/disk.go b/lxd/device/disk.go
index 051753f57f..790618cc4a 100644
--- a/lxd/device/disk.go
+++ b/lxd/device/disk.go
@@ -75,11 +75,11 @@ func (d *disk) validateConfig(instConf instance.ConfigReader) error {
 	}
 
 	rules := map[string]func(string) error{
-		"required":          validate.IsBool,
-		"optional":          validate.IsBool, // "optional" is deprecated, replaced by "required".
-		"readonly":          validate.IsBool,
-		"recursive":         validate.IsBool,
-		"shift":             validate.IsBool,
+		"required":          validate.Optional(validate.IsBool),
+		"optional":          validate.Optional(validate.IsBool), // "optional" is deprecated, replaced by "required".
+		"readonly":          validate.Optional(validate.IsBool),
+		"recursive":         validate.Optional(validate.IsBool),
+		"shift":             validate.Optional(validate.IsBool),
 		"source":            validate.IsAny,
 		"limits.read":       validate.IsAny,
 		"limits.write":      validate.IsAny,
diff --git a/lxd/device/proxy.go b/lxd/device/proxy.go
index 349d2307b6..c3ef87019c 100644
--- a/lxd/device/proxy.go
+++ b/lxd/device/proxy.go
@@ -73,12 +73,12 @@ func (d *proxy) validateConfig(instConf instance.ConfigReader) error {
 		"connect":        validateAddr,
 		"bind":           validateBind,
 		"mode":           unixValidOctalFileMode,
-		"nat":            validate.IsBool,
+		"nat":            validate.Optional(validate.IsBool),
 		"gid":            unixValidUserID,
 		"uid":            unixValidUserID,
 		"security.uid":   unixValidUserID,
 		"security.gid":   unixValidUserID,
-		"proxy_protocol": validate.IsBool,
+		"proxy_protocol": validate.Optional(validate.IsBool),
 	}
 
 	err := d.config.Validate(rules)
diff --git a/lxd/device/unix_common.go b/lxd/device/unix_common.go
index 80a369521c..156e5d2600 100644
--- a/lxd/device/unix_common.go
+++ b/lxd/device/unix_common.go
@@ -54,7 +54,7 @@ func (d *unixCommon) validateConfig(instConf instance.ConfigReader) error {
 		"uid":      unixValidUserID,
 		"gid":      unixValidUserID,
 		"mode":     unixValidOctalFileMode,
-		"required": validate.IsBool,
+		"required": validate.Optional(validate.IsBool),
 	}
 
 	err := d.config.Validate(rules)
diff --git a/lxd/device/unix_hotplug.go b/lxd/device/unix_hotplug.go
index dd7db877e7..71f62b6f0b 100644
--- a/lxd/device/unix_hotplug.go
+++ b/lxd/device/unix_hotplug.go
@@ -53,7 +53,7 @@ func (d *unixHotplug) validateConfig(instConf instance.ConfigReader) error {
 		"uid":       unixValidUserID,
 		"gid":       unixValidUserID,
 		"mode":      unixValidOctalFileMode,
-		"required":  validate.IsBool,
+		"required":  validate.Optional(validate.IsBool),
 	}
 
 	err := d.config.Validate(rules)
diff --git a/lxd/device/usb.go b/lxd/device/usb.go
index 7dcfba9626..155e62cec6 100644
--- a/lxd/device/usb.go
+++ b/lxd/device/usb.go
@@ -55,7 +55,7 @@ func (d *usb) validateConfig(instConf instance.ConfigReader) error {
 		"uid":       unixValidUserID,
 		"gid":       unixValidUserID,
 		"mode":      unixValidOctalFileMode,
-		"required":  validate.IsBool,
+		"required":  validate.Optional(validate.IsBool),
 	}
 
 	err := d.config.Validate(rules)
diff --git a/lxd/network/driver_bridge.go b/lxd/network/driver_bridge.go
index 6199ed65fe..0d466a3203 100644
--- a/lxd/network/driver_bridge.go
+++ b/lxd/network/driver_bridge.go
@@ -177,18 +177,18 @@ func (n *bridge) Validate(config map[string]string) error {
 
 			return validate.IsNetworkAddressCIDRV4(value)
 		},
-		"ipv4.firewall": validate.IsBool,
-		"ipv4.nat":      validate.IsBool,
+		"ipv4.firewall": validate.Optional(validate.IsBool),
+		"ipv4.nat":      validate.Optional(validate.IsBool),
 		"ipv4.nat.order": func(value string) error {
 			return validate.IsOneOf(value, []string{"before", "after"})
 		},
 		"ipv4.nat.address":  validate.IsNetworkAddressV4,
-		"ipv4.dhcp":         validate.IsBool,
+		"ipv4.dhcp":         validate.Optional(validate.IsBool),
 		"ipv4.dhcp.gateway": validate.IsNetworkAddressV4,
 		"ipv4.dhcp.expiry":  validate.IsAny,
 		"ipv4.dhcp.ranges":  validate.IsAny,
 		"ipv4.routes":       validate.IsNetworkV4List,
-		"ipv4.routing":      validate.IsBool,
+		"ipv4.routing":      validate.Optional(validate.IsBool),
 
 		"ipv6.address": func(value string) error {
 			if validate.IsOneOf(value, []string{"none", "auto"}) == nil {
@@ -197,18 +197,18 @@ func (n *bridge) Validate(config map[string]string) error {
 
 			return validate.IsNetworkAddressCIDRV6(value)
 		},
-		"ipv6.firewall": validate.IsBool,
-		"ipv6.nat":      validate.IsBool,
+		"ipv6.firewall": validate.Optional(validate.IsBool),
+		"ipv6.nat":      validate.Optional(validate.IsBool),
 		"ipv6.nat.order": func(value string) error {
 			return validate.IsOneOf(value, []string{"before", "after"})
 		},
 		"ipv6.nat.address":   validate.IsNetworkAddressV6,
-		"ipv6.dhcp":          validate.IsBool,
+		"ipv6.dhcp":          validate.Optional(validate.IsBool),
 		"ipv6.dhcp.expiry":   validate.IsAny,
-		"ipv6.dhcp.stateful": validate.IsBool,
+		"ipv6.dhcp.stateful": validate.Optional(validate.IsBool),
 		"ipv6.dhcp.ranges":   validate.IsAny,
 		"ipv6.routes":        validate.IsNetworkV6List,
-		"ipv6.routing":       validate.IsBool,
+		"ipv6.routing":       validate.Optional(validate.IsBool),
 
 		"dns.domain": validate.IsAny,
 		"dns.search": validate.IsAny,
diff --git a/lxd/storage/drivers/driver_ceph.go b/lxd/storage/drivers/driver_ceph.go
index f5d7da67c2..f0b97bcdce 100644
--- a/lxd/storage/drivers/driver_ceph.go
+++ b/lxd/storage/drivers/driver_ceph.go
@@ -239,11 +239,11 @@ func (d *ceph) Delete(op *operations.Operation) error {
 func (d *ceph) Validate(config map[string]string) error {
 	rules := map[string]func(value string) error{
 		"ceph.cluster_name":       validate.IsAny,
-		"ceph.osd.force_reuse":    validate.IsBool,
+		"ceph.osd.force_reuse":    validate.Optional(validate.IsBool),
 		"ceph.osd.pg_num":         validate.IsAny,
 		"ceph.osd.pool_name":      validate.IsAny,
 		"ceph.osd.data_pool_name": validate.IsAny,
-		"ceph.rbd.clone_copy":     validate.IsBool,
+		"ceph.rbd.clone_copy":     validate.Optional(validate.IsBool),
 		"ceph.user.name":          validate.IsAny,
 		"volatile.pool.pristine":  validate.IsAny,
 		"volume.block.filesystem": func(value string) error {
diff --git a/lxd/storage/drivers/driver_lvm.go b/lxd/storage/drivers/driver_lvm.go
index 8798a3076e..5f25525ae9 100644
--- a/lxd/storage/drivers/driver_lvm.go
+++ b/lxd/storage/drivers/driver_lvm.go
@@ -431,7 +431,7 @@ func (d *lvm) Validate(config map[string]string) error {
 	rules := map[string]func(value string) error{
 		"lvm.vg_name":                validate.IsAny,
 		"lvm.thinpool_name":          validate.IsAny,
-		"lvm.use_thinpool":           validate.IsBool,
+		"lvm.use_thinpool":           validate.Optional(validate.IsBool),
 		"volume.block.mount_options": validate.IsAny,
 		"volume.block.filesystem": func(value string) error {
 			if value == "" {
@@ -441,7 +441,7 @@ func (d *lvm) Validate(config map[string]string) error {
 		},
 		"volume.lvm.stripes":      validate.Optional(validate.IsUint32),
 		"volume.lvm.stripes.size": validate.IsSize,
-		"lvm.vg.force_reuse":      validate.IsBool,
+		"lvm.vg.force_reuse":      validate.Optional(validate.IsBool),
 	}
 
 	err := d.validatePool(config, rules)
diff --git a/lxd/storage/drivers/driver_zfs.go b/lxd/storage/drivers/driver_zfs.go
index 9d9fbde25e..73be97127d 100644
--- a/lxd/storage/drivers/driver_zfs.go
+++ b/lxd/storage/drivers/driver_zfs.go
@@ -343,9 +343,9 @@ func (d *zfs) Delete(op *operations.Operation) error {
 func (d *zfs) Validate(config map[string]string) error {
 	rules := map[string]func(value string) error{
 		"zfs.pool_name":               validate.IsAny,
-		"zfs.clone_copy":              validate.IsBool,
-		"volume.zfs.remove_snapshots": validate.IsBool,
-		"volume.zfs.use_refquota":     validate.IsBool,
+		"zfs.clone_copy":              validate.Optional(validate.IsBool),
+		"volume.zfs.remove_snapshots": validate.Optional(validate.IsBool),
+		"volume.zfs.use_refquota":     validate.Optional(validate.IsBool),
 	}
 
 	return d.validatePool(config, rules)
diff --git a/lxd/storage/drivers/driver_zfs_volumes.go b/lxd/storage/drivers/driver_zfs_volumes.go
index a32c05df3c..082ae886d2 100644
--- a/lxd/storage/drivers/driver_zfs_volumes.go
+++ b/lxd/storage/drivers/driver_zfs_volumes.go
@@ -796,8 +796,8 @@ func (d *zfs) HasVolume(vol Volume) bool {
 // ValidateVolume validates the supplied volume config.
 func (d *zfs) ValidateVolume(vol Volume, removeUnknownKeys bool) error {
 	rules := map[string]func(value string) error{
-		"zfs.remove_snapshots": validate.IsBool,
-		"zfs.use_refquota":     validate.IsBool,
+		"zfs.remove_snapshots": validate.Optional(validate.IsBool),
+		"zfs.use_refquota":     validate.Optional(validate.IsBool),
 	}
 
 	return d.validateVolume(vol, rules, removeUnknownKeys)
diff --git a/lxd/storage/utils.go b/lxd/storage/utils.go
index 9c93f8e69c..27be7d084b 100644
--- a/lxd/storage/utils.go
+++ b/lxd/storage/utils.go
@@ -251,10 +251,10 @@ var StorageVolumeConfigKeys = map[string]func(value string) ([]string, error){
 		return []string{"ceph", "lvm"}, validate.IsAny(value)
 	},
 	"security.shifted": func(value string) ([]string, error) {
-		return SupportedPoolTypes, validate.IsBool(value)
+		return SupportedPoolTypes, validate.Optional(validate.IsBool)(value)
 	},
 	"security.unmapped": func(value string) ([]string, error) {
-		return SupportedPoolTypes, validate.IsBool(value)
+		return SupportedPoolTypes, validate.Optional(validate.IsBool)(value)
 	},
 	"size": func(value string) ([]string, error) {
 		if value == "" {
@@ -275,7 +275,7 @@ var StorageVolumeConfigKeys = map[string]func(value string) ([]string, error){
 		return SupportedPoolTypes, validate.IsAny(value)
 	},
 	"zfs.remove_snapshots": func(value string) ([]string, error) {
-		err := validate.IsBool(value)
+		err := validate.Optional(validate.IsBool)(value)
 		if err != nil {
 			return nil, err
 		}
@@ -283,7 +283,7 @@ var StorageVolumeConfigKeys = map[string]func(value string) ([]string, error){
 		return []string{"zfs"}, nil
 	},
 	"zfs.use_refquota": func(value string) ([]string, error) {
-		err := validate.IsBool(value)
+		err := validate.Optional(validate.IsBool)(value)
 		if err != nil {
 			return nil, err
 		}
@@ -473,8 +473,8 @@ func validateVolumeCommonRules(vol drivers.Volume) map[string]func(string) error
 
 	// security.shifted and security.unmapped are only relevant for custom volumes.
 	if vol.Type() == drivers.VolumeTypeCustom {
-		rules["security.shifted"] = validate.IsBool
-		rules["security.unmapped"] = validate.IsBool
+		rules["security.shifted"] = validate.Optional(validate.IsBool)
+		rules["security.unmapped"] = validate.Optional(validate.IsBool)
 	}
 
 	// volatile.rootfs.size is only used for image volumes.
diff --git a/lxd/storage_pools_config.go b/lxd/storage_pools_config.go
index 0dc57cf710..2231de9985 100644
--- a/lxd/storage_pools_config.go
+++ b/lxd/storage_pools_config.go
@@ -23,7 +23,7 @@ var storagePoolConfigKeys = map[string]func(value string) error{
 
 	// valid drivers: ceph
 	"ceph.cluster_name":       validate.IsAny,
-	"ceph.osd.force_reuse":    validate.IsBool,
+	"ceph.osd.force_reuse":    validate.Optional(validate.IsBool),
 	"ceph.osd.pool_name":      validate.IsAny,
 	"ceph.osd.data_pool_name": validate.IsAny,
 	"ceph.osd.pg_num": func(value string) error {
@@ -34,7 +34,7 @@ var storagePoolConfigKeys = map[string]func(value string) error{
 		_, err := units.ParseByteSizeString(value)
 		return err
 	},
-	"ceph.rbd.clone_copy": validate.IsBool,
+	"ceph.rbd.clone_copy": validate.Optional(validate.IsBool),
 	"ceph.user.name":      validate.IsAny,
 
 	// valid drivers: cephfs
@@ -44,11 +44,11 @@ var storagePoolConfigKeys = map[string]func(value string) error{
 
 	// valid drivers: lvm
 	"lvm.thinpool_name":       validate.IsAny,
-	"lvm.use_thinpool":        validate.IsBool,
+	"lvm.use_thinpool":        validate.Optional(validate.IsBool),
 	"lvm.vg_name":             validate.IsAny,
 	"volume.lvm.stripes":      validate.Optional(validate.IsUint32),
 	"volume.lvm.stripes.size": validate.IsSize,
-	"lvm.vg.force_reuse":      validate.IsBool,
+	"lvm.vg.force_reuse":      validate.Optional(validate.IsBool),
 
 	// valid drivers: btrfs, lvm, zfs
 	"size": validate.IsSize,
@@ -74,11 +74,11 @@ var storagePoolConfigKeys = map[string]func(value string) error{
 	"volume.size": validate.IsSize,
 
 	// valid drivers: zfs
-	"volume.zfs.remove_snapshots": validate.IsBool,
-	"volume.zfs.use_refquota":     validate.IsBool,
+	"volume.zfs.remove_snapshots": validate.Optional(validate.IsBool),
+	"volume.zfs.use_refquota":     validate.Optional(validate.IsBool),
 
 	// valid drivers: zfs
-	"zfs.clone_copy": validate.IsBool,
+	"zfs.clone_copy": validate.Optional(validate.IsBool),
 	"zfs.pool_name":  validate.IsAny,
 	"rsync.bwlimit":  validate.IsAny,
 }

From 9c934239621ef9fc921a45e38768533fc03b573e Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Mon, 3 Aug 2020 11:57:57 +0100
Subject: [PATCH 10/33] shared/instance: Wraps validate.IsBool in
 validate.Optional

Due to IsBool being required now.

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

diff --git a/shared/instance.go b/shared/instance.go
index 306bd58fac..b596cbae6b 100644
--- a/shared/instance.go
+++ b/shared/instance.go
@@ -71,7 +71,7 @@ var HugePageSizeSuffix = [...]string{"64KB", "1MB", "2MB", "1GB"}
 // to an appropriate checker function, which validates whether or not a
 // given value is syntactically legal.
 var KnownInstanceConfigKeys = map[string]func(value string) error{
-	"boot.autostart":             validate.IsBool,
+	"boot.autostart":             validate.Optional(validate.IsBool),
 	"boot.autostart.delay":       validate.Optional(validate.IsInt64),
 	"boot.autostart.priority":    validate.Optional(validate.IsInt64),
 	"boot.stop.priority":         validate.Optional(validate.IsInt64),
@@ -166,9 +166,9 @@ var KnownInstanceConfigKeys = map[string]func(value string) error{
 	"limits.memory.enforce": func(value string) error {
 		return validate.IsOneOf(value, []string{"soft", "hard"})
 	},
-	"limits.memory.swap":          validate.IsBool,
+	"limits.memory.swap":          validate.Optional(validate.IsBool),
 	"limits.memory.swap.priority": validate.Optional(validate.IsPriority),
-	"limits.memory.hugepages":     validate.IsBool,
+	"limits.memory.hugepages":     validate.Optional(validate.IsBool),
 
 	"limits.network.priority": validate.Optional(validate.IsPriority),
 
@@ -176,42 +176,42 @@ var KnownInstanceConfigKeys = map[string]func(value string) error{
 
 	"linux.kernel_modules": validate.IsAny,
 
-	"migration.incremental.memory":            validate.IsBool,
+	"migration.incremental.memory":            validate.Optional(validate.IsBool),
 	"migration.incremental.memory.iterations": validate.Optional(validate.IsUint32),
 	"migration.incremental.memory.goal":       validate.Optional(validate.IsUint32),
 
-	"nvidia.runtime":             validate.IsBool,
+	"nvidia.runtime":             validate.Optional(validate.IsBool),
 	"nvidia.driver.capabilities": validate.IsAny,
 	"nvidia.require.cuda":        validate.IsAny,
 	"nvidia.require.driver":      validate.IsAny,
 
-	"security.nesting":       validate.IsBool,
-	"security.privileged":    validate.IsBool,
-	"security.devlxd":        validate.IsBool,
-	"security.devlxd.images": validate.IsBool,
+	"security.nesting":       validate.Optional(validate.IsBool),
+	"security.privileged":    validate.Optional(validate.IsBool),
+	"security.devlxd":        validate.Optional(validate.IsBool),
+	"security.devlxd.images": validate.Optional(validate.IsBool),
 
-	"security.protection.delete": validate.IsBool,
-	"security.protection.shift":  validate.IsBool,
+	"security.protection.delete": validate.Optional(validate.IsBool),
+	"security.protection.shift":  validate.Optional(validate.IsBool),
 
 	"security.idmap.base":     validate.Optional(validate.IsUint32),
-	"security.idmap.isolated": validate.IsBool,
+	"security.idmap.isolated": validate.Optional(validate.IsBool),
 	"security.idmap.size":     validate.Optional(validate.IsUint32),
 
-	"security.secureboot": validate.IsBool,
+	"security.secureboot": validate.Optional(validate.IsBool),
 
 	"security.syscalls.allow":                   validate.IsAny,
-	"security.syscalls.blacklist_default":       validate.IsBool,
-	"security.syscalls.blacklist_compat":        validate.IsBool,
+	"security.syscalls.blacklist_default":       validate.Optional(validate.IsBool),
+	"security.syscalls.blacklist_compat":        validate.Optional(validate.IsBool),
 	"security.syscalls.blacklist":               validate.IsAny,
-	"security.syscalls.deny_default":            validate.IsBool,
-	"security.syscalls.deny_compat":             validate.IsBool,
+	"security.syscalls.deny_default":            validate.Optional(validate.IsBool),
+	"security.syscalls.deny_compat":             validate.Optional(validate.IsBool),
 	"security.syscalls.deny":                    validate.IsAny,
-	"security.syscalls.intercept.mknod":         validate.IsBool,
-	"security.syscalls.intercept.mount":         validate.IsBool,
+	"security.syscalls.intercept.mknod":         validate.Optional(validate.IsBool),
+	"security.syscalls.intercept.mount":         validate.Optional(validate.IsBool),
 	"security.syscalls.intercept.mount.allowed": validate.IsAny,
 	"security.syscalls.intercept.mount.fuse":    validate.IsAny,
-	"security.syscalls.intercept.mount.shift":   validate.IsBool,
-	"security.syscalls.intercept.setxattr":      validate.IsBool,
+	"security.syscalls.intercept.mount.shift":   validate.Optional(validate.IsBool),
+	"security.syscalls.intercept.setxattr":      validate.Optional(validate.IsBool),
 	"security.syscalls.whitelist":               validate.IsAny,
 
 	"snapshots.schedule": func(value string) error {
@@ -230,7 +230,7 @@ var KnownInstanceConfigKeys = map[string]func(value string) error{
 
 		return nil
 	},
-	"snapshots.schedule.stopped": validate.IsBool,
+	"snapshots.schedule.stopped": validate.Optional(validate.IsBool),
 	"snapshots.pattern":          validate.IsAny,
 	"snapshots.expiry": func(value string) error {
 		// Validate expression

From 377e61db55761f763a3589883cb7ac4fb51596f1 Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Mon, 3 Aug 2020 12:02:55 +0100
Subject: [PATCH 11/33] shared/validate: Makes IsSize non-optional

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

diff --git a/shared/validate/validate.go b/shared/validate/validate.go
index cacdb38ab1..fb1075e423 100644
--- a/shared/validate/validate.go
+++ b/shared/validate/validate.go
@@ -113,10 +113,6 @@ func IsNotEmpty(value string) error {
 
 // IsSize checks if string is valid size according to units.ParseByteSizeString.
 func IsSize(value string) error {
-	if value == "" {
-		return nil
-	}
-
 	_, err := units.ParseByteSizeString(value)
 	if err != nil {
 		return err

From ab374f269c961531ca6a722b4374b4a07de6301d Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Mon, 3 Aug 2020 12:03:46 +0100
Subject: [PATCH 12/33] lxd: Wraps validate.IsSize in validate.Optional

Now that validate.IsSize is non-optional.

Signed-off-by: Thomas Parrott <thomas.parrott at canonical.com>
---
 lxd/api_project.go                        | 4 ++--
 lxd/storage/drivers/driver_lvm.go         | 2 +-
 lxd/storage/drivers/driver_lvm_volumes.go | 2 +-
 lxd/storage/utils.go                      | 6 +++---
 lxd/storage_pools_config.go               | 6 +++---
 5 files changed, 10 insertions(+), 10 deletions(-)

diff --git a/lxd/api_project.go b/lxd/api_project.go
index c4541ac8c4..25b43c95c1 100644
--- a/lxd/api_project.go
+++ b/lxd/api_project.go
@@ -528,10 +528,10 @@ var projectConfigKeys = map[string]func(value string) error{
 	"features.storage.volumes":       validate.Optional(validate.IsBool),
 	"limits.containers":              validate.Optional(validate.IsUint32),
 	"limits.virtual-machines":        validate.Optional(validate.IsUint32),
-	"limits.memory":                  validate.IsSize,
+	"limits.memory":                  validate.Optional(validate.IsSize),
 	"limits.processes":               validate.Optional(validate.IsUint32),
 	"limits.cpu":                     validate.Optional(validate.IsUint32),
-	"limits.disk":                    validate.IsSize,
+	"limits.disk":                    validate.Optional(validate.IsSize),
 	"restricted":                     validate.Optional(validate.IsBool),
 	"restricted.containers.nesting":  isEitherAllowOrBlock,
 	"restricted.containers.lowlevel": isEitherAllowOrBlock,
diff --git a/lxd/storage/drivers/driver_lvm.go b/lxd/storage/drivers/driver_lvm.go
index 5f25525ae9..2082cc8cdf 100644
--- a/lxd/storage/drivers/driver_lvm.go
+++ b/lxd/storage/drivers/driver_lvm.go
@@ -440,7 +440,7 @@ func (d *lvm) Validate(config map[string]string) error {
 			return validate.IsOneOf(value, lvmAllowedFilesystems)
 		},
 		"volume.lvm.stripes":      validate.Optional(validate.IsUint32),
-		"volume.lvm.stripes.size": validate.IsSize,
+		"volume.lvm.stripes.size": validate.Optional(validate.IsSize),
 		"lvm.vg.force_reuse":      validate.Optional(validate.IsBool),
 	}
 
diff --git a/lxd/storage/drivers/driver_lvm_volumes.go b/lxd/storage/drivers/driver_lvm_volumes.go
index 2d9c730076..fbe1ef16da 100644
--- a/lxd/storage/drivers/driver_lvm_volumes.go
+++ b/lxd/storage/drivers/driver_lvm_volumes.go
@@ -249,7 +249,7 @@ func (d *lvm) ValidateVolume(vol Volume, removeUnknownKeys bool) error {
 			return validate.IsOneOf(value, lvmAllowedFilesystems)
 		},
 		"lvm.stripes":      validate.Optional(validate.IsUint32),
-		"lvm.stripes.size": validate.IsSize,
+		"lvm.stripes.size": validate.Optional(validate.IsSize),
 	}
 
 	err := d.validateVolume(vol, rules, removeUnknownKeys)
diff --git a/lxd/storage/utils.go b/lxd/storage/utils.go
index 27be7d084b..19e8ba818a 100644
--- a/lxd/storage/utils.go
+++ b/lxd/storage/utils.go
@@ -421,8 +421,8 @@ func validatePoolCommonRules() map[string]func(string) error {
 	return map[string]func(string) error{
 		"source":                  validate.IsAny,
 		"volatile.initial_source": validate.IsAny,
-		"volume.size":             validate.IsSize,
-		"size":                    validate.IsSize,
+		"volume.size":             validate.Optional(validate.IsSize),
+		"size":                    validate.Optional(validate.IsSize),
 		"rsync.bwlimit":           validate.IsAny,
 	}
 }
@@ -435,7 +435,7 @@ func validateVolumeCommonRules(vol drivers.Volume) map[string]func(string) error
 
 		// Note: size should not be modifiable for non-custom volumes and should be checked
 		// in the relevant volume update functions.
-		"size": validate.IsSize,
+		"size": validate.Optional(validate.IsSize),
 
 		"snapshots.expiry": func(value string) error {
 			// Validate expression
diff --git a/lxd/storage_pools_config.go b/lxd/storage_pools_config.go
index 2231de9985..cfcdf0b234 100644
--- a/lxd/storage_pools_config.go
+++ b/lxd/storage_pools_config.go
@@ -47,11 +47,11 @@ var storagePoolConfigKeys = map[string]func(value string) error{
 	"lvm.use_thinpool":        validate.Optional(validate.IsBool),
 	"lvm.vg_name":             validate.IsAny,
 	"volume.lvm.stripes":      validate.Optional(validate.IsUint32),
-	"volume.lvm.stripes.size": validate.IsSize,
+	"volume.lvm.stripes.size": validate.Optional(validate.IsSize),
 	"lvm.vg.force_reuse":      validate.Optional(validate.IsBool),
 
 	// valid drivers: btrfs, lvm, zfs
-	"size": validate.IsSize,
+	"size": validate.Optional(validate.IsSize),
 
 	// valid drivers: btrfs, dir, lvm, zfs
 	"source": validate.IsAny,
@@ -71,7 +71,7 @@ var storagePoolConfigKeys = map[string]func(value string) error{
 	"volume.block.mount_options": validate.IsAny,
 
 	// valid drivers: ceph, lvm
-	"volume.size": validate.IsSize,
+	"volume.size": validate.Optional(validate.IsSize),
 
 	// valid drivers: zfs
 	"volume.zfs.remove_snapshots": validate.Optional(validate.IsBool),

From d1adf48d8ff7760048bcfdf933c8d0b59cb7d0af Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Mon, 3 Aug 2020 12:04:10 +0100
Subject: [PATCH 13/33] shared/instance: Wraps validate.IsSize in
 validate.Optional

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

diff --git a/shared/instance.go b/shared/instance.go
index b596cbae6b..42eba631c1 100644
--- a/shared/instance.go
+++ b/shared/instance.go
@@ -137,10 +137,10 @@ var KnownInstanceConfigKeys = map[string]func(value string) error{
 
 	"limits.disk.priority": validate.Optional(validate.IsPriority),
 
-	"limits.hugepages.64KB": validate.IsSize,
-	"limits.hugepages.1MB":  validate.IsSize,
-	"limits.hugepages.2MB":  validate.IsSize,
-	"limits.hugepages.1GB":  validate.IsSize,
+	"limits.hugepages.64KB": validate.Optional(validate.IsSize),
+	"limits.hugepages.1MB":  validate.Optional(validate.IsSize),
+	"limits.hugepages.2MB":  validate.Optional(validate.IsSize),
+	"limits.hugepages.1GB":  validate.Optional(validate.IsSize),
 
 	"limits.memory": func(value string) error {
 		if value == "" {

From 8176f505dec5950f196d3024615c1d30ba4792bf Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Mon, 3 Aug 2020 12:04:46 +0100
Subject: [PATCH 14/33] shared/validate: Makes IsNetworkAddress non-optional

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

diff --git a/shared/validate/validate.go b/shared/validate/validate.go
index fb1075e423..9820671356 100644
--- a/shared/validate/validate.go
+++ b/shared/validate/validate.go
@@ -153,10 +153,6 @@ func IsNetworkMAC(value string) error {
 
 // IsNetworkAddress validates an IP (v4 or v6) address string. If string is empty, returns valid.
 func IsNetworkAddress(value string) error {
-	if value == "" {
-		return nil
-	}
-
 	ip := net.ParseIP(value)
 	if ip == nil {
 		return fmt.Errorf("Not an IP address %q", value)

From 4b9bc47c7c5f4a2a7ebed30a58ed20260118507a Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Mon, 3 Aug 2020 12:11:17 +0100
Subject: [PATCH 15/33] lxd: Wraps validate.IsNetworkAddress in
 validate.Optional

Signed-off-by: Thomas Parrott <thomas.parrott at canonical.com>
---
 lxd/device/device_utils_proxy.go | 2 +-
 lxd/network/driver_bridge.go     | 6 +++---
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/lxd/device/device_utils_proxy.go b/lxd/device/device_utils_proxy.go
index 47455ffd94..6aae2a5b87 100644
--- a/lxd/device/device_utils_proxy.go
+++ b/lxd/device/device_utils_proxy.go
@@ -38,7 +38,7 @@ func ProxyParseAddr(addr string) (*deviceConfig.ProxyAddress, error) {
 
 	// Validate that it's a valid address.
 	if shared.StringInSlice(newProxyAddr.ConnType, []string{"udp", "tcp"}) {
-		err := validate.IsNetworkAddress(address)
+		err := validate.Optional(validate.IsNetworkAddress)(address)
 		if err != nil {
 			return nil, err
 		}
diff --git a/lxd/network/driver_bridge.go b/lxd/network/driver_bridge.go
index 0d466a3203..5c2e00c9fc 100644
--- a/lxd/network/driver_bridge.go
+++ b/lxd/network/driver_bridge.go
@@ -245,13 +245,13 @@ func (n *bridge) Validate(config map[string]string) error {
 					return validate.IsOneOf(value, []string{"gre", "vxlan"})
 				}
 			case "local":
-				rules[k] = validate.IsNetworkAddress
+				rules[k] = validate.Optional(validate.IsNetworkAddress)
 			case "remote":
-				rules[k] = validate.IsNetworkAddress
+				rules[k] = validate.Optional(validate.IsNetworkAddress)
 			case "port":
 				rules[k] = networkValidPort
 			case "group":
-				rules[k] = validate.IsNetworkAddress
+				rules[k] = validate.Optional(validate.IsNetworkAddress)
 			case "id":
 				rules[k] = validate.Optional(validate.IsInt64)
 			case "inteface":

From ad0ab8ef7566506bcda34a44faccb5705997eaa2 Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Mon, 3 Aug 2020 12:11:45 +0100
Subject: [PATCH 16/33] shared/validate: Makes IsNetworkV4 non-optional

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

diff --git a/shared/validate/validate.go b/shared/validate/validate.go
index 9820671356..d21e748c7a 100644
--- a/shared/validate/validate.go
+++ b/shared/validate/validate.go
@@ -163,10 +163,6 @@ func IsNetworkAddress(value string) error {
 
 // IsNetworkV4 validates an IPv4 CIDR string. If string is empty, returns valid.
 func IsNetworkV4(value string) error {
-	if value == "" {
-		return nil
-	}
-
 	ip, subnet, err := net.ParseCIDR(value)
 	if err != nil {
 		return err

From 3e56783e8b79e7d1e899b5abb04dd8e066976476 Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Mon, 3 Aug 2020 12:15:46 +0100
Subject: [PATCH 17/33] lxd/network/driver/bridge: Wraps validate.IsNetworkV4
 in shared.Optional

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

diff --git a/lxd/network/driver_bridge.go b/lxd/network/driver_bridge.go
index 5c2e00c9fc..065faf50de 100644
--- a/lxd/network/driver_bridge.go
+++ b/lxd/network/driver_bridge.go
@@ -158,13 +158,13 @@ func (n *bridge) Validate(config map[string]string) error {
 			return validate.IsOneOf(value, []string{"standard", "fan"})
 		},
 
-		"fan.overlay_subnet": validate.IsNetworkV4,
+		"fan.overlay_subnet": validate.Optional(validate.IsNetworkV4),
 		"fan.underlay_subnet": func(value string) error {
 			if value == "auto" {
 				return nil
 			}
 
-			return validate.IsNetworkV4(value)
+			return validate.Optional(validate.IsNetworkV4)(value)
 		},
 		"fan.type": func(value string) error {
 			return validate.IsOneOf(value, []string{"vxlan", "ipip"})

From d0c933c0c2d44f1e554b227a46da3a1921c57168 Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Mon, 3 Aug 2020 12:16:26 +0100
Subject: [PATCH 18/33] shared/validate: Makes IsNetworkAddressV4 non-optional

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

diff --git a/shared/validate/validate.go b/shared/validate/validate.go
index d21e748c7a..a218eb7264 100644
--- a/shared/validate/validate.go
+++ b/shared/validate/validate.go
@@ -181,10 +181,6 @@ func IsNetworkV4(value string) error {
 
 // IsNetworkAddressV4 validates an IPv4 addresss string. If string is empty, returns valid.
 func IsNetworkAddressV4(value string) error {
-	if value == "" {
-		return nil
-	}
-
 	ip := net.ParseIP(value)
 	if ip == nil || ip.To4() == nil {
 		return fmt.Errorf("Not an IPv4 address: %s", value)

From 081b4978f0ecd07bcec3dacad6a6560aadbee502 Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Mon, 3 Aug 2020 12:29:37 +0100
Subject: [PATCH 19/33] lxd/device/nic: Wraps validate.IsNetworkAddressV4 in
 validate.Optional

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

diff --git a/lxd/device/nic.go b/lxd/device/nic.go
index 6e933bc443..5a15c3e4b1 100644
--- a/lxd/device/nic.go
+++ b/lxd/device/nic.go
@@ -23,14 +23,14 @@ func nicValidationRules(requiredFields []string, optionalFields []string) map[st
 		"security.ipv6_filtering": validate.IsAny,
 		"maas.subnet.ipv4":        validate.IsAny,
 		"maas.subnet.ipv6":        validate.IsAny,
-		"ipv4.address":            validate.IsNetworkAddressV4,
+		"ipv4.address":            validate.Optional(validate.IsNetworkAddressV4),
 		"ipv6.address":            validate.IsNetworkAddressV6,
 		"ipv4.routes":             validate.IsNetworkV4List,
 		"ipv6.routes":             validate.IsNetworkV6List,
 		"boot.priority":           validate.Optional(validate.IsUint32),
 		"ipv4.gateway":            networkValidGateway,
 		"ipv6.gateway":            networkValidGateway,
-		"ipv4.host_address":       validate.IsNetworkAddressV4,
+		"ipv4.host_address":       validate.Optional(validate.IsNetworkAddressV4),
 		"ipv6.host_address":       validate.IsNetworkAddressV6,
 		"ipv4.host_table":         validate.Optional(validate.IsUint32),
 		"ipv6.host_table":         validate.Optional(validate.IsUint32),

From 80a844dcc933e2158d6aa2811b318fde0a8c142b Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Mon, 3 Aug 2020 12:30:13 +0100
Subject: [PATCH 20/33] lxd/device/nic/ipvlan: Wraps
 validate.IsNetworkAddressV4 in validate.Optional

Removes duplicated empty check.

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

diff --git a/lxd/device/nic_ipvlan.go b/lxd/device/nic_ipvlan.go
index 26657dd667..9b25e904c8 100644
--- a/lxd/device/nic_ipvlan.go
+++ b/lxd/device/nic_ipvlan.go
@@ -117,11 +117,7 @@ func (d *nicIPVLAN) validateConfig(instConf instance.ConfigReader) error {
 
 	if d.config["mode"] == ipvlanModeL2 {
 		rules["ipv4.gateway"] = func(value string) error {
-			if value == "" {
-				return nil
-			}
-
-			return validate.IsNetworkAddressV4(value)
+			return validate.Optional(validate.IsNetworkAddressV4)(value)
 		}
 
 		rules["ipv6.gateway"] = func(value string) error {

From b0627a1ed6fb4e903d56a327cfe6863990f1a293 Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Mon, 3 Aug 2020 12:30:42 +0100
Subject: [PATCH 21/33] lxd/device/nic/ipvlan: Fixes incorrect IPv4 address
 check in IPv6 context

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

diff --git a/lxd/device/nic_ipvlan.go b/lxd/device/nic_ipvlan.go
index 9b25e904c8..e21efcfed4 100644
--- a/lxd/device/nic_ipvlan.go
+++ b/lxd/device/nic_ipvlan.go
@@ -303,7 +303,7 @@ func (d *nicIPVLAN) Start() (*deviceConfig.RunConfig, error) {
 				addr = fmt.Sprintf("%s/128", addr)
 			}
 
-			if mode == "l2" && validate.IsNetworkAddressV4(addr) == nil {
+			if mode == "l2" && validate.IsNetworkAddressV6(addr) == nil {
 				addr = fmt.Sprintf("%s/64", addr)
 			}
 

From c2222650f985d46f91301f52b380357d28c9df18 Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Mon, 3 Aug 2020 12:31:17 +0100
Subject: [PATCH 22/33] lxd/network/driver/bridge: Wraps
 validate.IsNetworkAddressV4 in validate.Optional

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

diff --git a/lxd/network/driver_bridge.go b/lxd/network/driver_bridge.go
index 065faf50de..3dc8d2c317 100644
--- a/lxd/network/driver_bridge.go
+++ b/lxd/network/driver_bridge.go
@@ -182,9 +182,9 @@ func (n *bridge) Validate(config map[string]string) error {
 		"ipv4.nat.order": func(value string) error {
 			return validate.IsOneOf(value, []string{"before", "after"})
 		},
-		"ipv4.nat.address":  validate.IsNetworkAddressV4,
+		"ipv4.nat.address":  validate.Optional(validate.IsNetworkAddressV4),
 		"ipv4.dhcp":         validate.Optional(validate.IsBool),
-		"ipv4.dhcp.gateway": validate.IsNetworkAddressV4,
+		"ipv4.dhcp.gateway": validate.Optional(validate.IsNetworkAddressV4),
 		"ipv4.dhcp.expiry":  validate.IsAny,
 		"ipv4.dhcp.ranges":  validate.IsAny,
 		"ipv4.routes":       validate.IsNetworkV4List,

From 89d24cc7cfadd0d13b0bbc714456eb293b62ccf0 Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Mon, 3 Aug 2020 12:32:08 +0100
Subject: [PATCH 23/33] shared/validate: Makes IsNetworkAddressCIDRV4
 non-optional

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

diff --git a/shared/validate/validate.go b/shared/validate/validate.go
index a218eb7264..e4ce08cefa 100644
--- a/shared/validate/validate.go
+++ b/shared/validate/validate.go
@@ -191,10 +191,6 @@ func IsNetworkAddressV4(value string) error {
 
 // IsNetworkAddressCIDRV4 validates an IPv4 addresss string in CIDR format. If string is empty, returns valid.
 func IsNetworkAddressCIDRV4(value string) error {
-	if value == "" {
-		return nil
-	}
-
 	ip, subnet, err := net.ParseCIDR(value)
 	if err != nil {
 		return err

From 1ede05b54b999aba32928be1c7b639f804081aeb Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Mon, 3 Aug 2020 13:45:07 +0100
Subject: [PATCH 24/33] lxd: Wraps validate.IsNetworkAddressCIDRV4 in
 validate.Optional

Signed-off-by: Thomas Parrott <thomas.parrott at canonical.com>
---
 lxd/main_init_interactive.go |   2 +-
 lxd/network/driver_bridge.go |   2 +-
 lxd/network/driver_ovn.go    | 778 +++++++++++++++++++++++++++++++++++
 3 files changed, 780 insertions(+), 2 deletions(-)
 create mode 100644 lxd/network/driver_ovn.go

diff --git a/lxd/main_init_interactive.go b/lxd/main_init_interactive.go
index cf548f264a..4414cb3241 100644
--- a/lxd/main_init_interactive.go
+++ b/lxd/main_init_interactive.go
@@ -371,7 +371,7 @@ func (c *cmdInit) askNetworking(config *cmdInitData, d lxd.InstanceServer) error
 				return nil
 			}
 
-			return validate.IsNetworkAddressCIDRV4(value)
+			return validate.Optional(validate.IsNetworkAddressCIDRV4)(value)
 		})
 
 		if !shared.StringInSlice(net.Config["ipv4.address"], []string{"auto", "none"}) {
diff --git a/lxd/network/driver_bridge.go b/lxd/network/driver_bridge.go
index 3dc8d2c317..1cb7773279 100644
--- a/lxd/network/driver_bridge.go
+++ b/lxd/network/driver_bridge.go
@@ -175,7 +175,7 @@ func (n *bridge) Validate(config map[string]string) error {
 				return nil
 			}
 
-			return validate.IsNetworkAddressCIDRV4(value)
+			return validate.Optional(validate.IsNetworkAddressCIDRV4)(value)
 		},
 		"ipv4.firewall": validate.Optional(validate.IsBool),
 		"ipv4.nat":      validate.Optional(validate.IsBool),
diff --git a/lxd/network/driver_ovn.go b/lxd/network/driver_ovn.go
new file mode 100644
index 0000000000..2da3430e90
--- /dev/null
+++ b/lxd/network/driver_ovn.go
@@ -0,0 +1,778 @@
+package network
+
+import (
+	"fmt"
+	"io/ioutil"
+	"net"
+	"strings"
+	"time"
+
+	"github.com/pkg/errors"
+
+	"github.com/lxc/lxd/lxd/dnsmasq/dhcpalloc"
+	"github.com/lxc/lxd/lxd/instance"
+	"github.com/lxc/lxd/lxd/network/openvswitch"
+	"github.com/lxc/lxd/lxd/project"
+	"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"
+)
+
+//ovnVars OVN object variables.
+type ovnVars struct {
+	// OVN client.
+	client *openvswitch.OVN
+
+	// Chassis.
+	chassisGroupName openvswitch.OVNChassisGroup
+
+	// Router.
+	routerName           openvswitch.OVNRouter
+	routerExtPortName    openvswitch.OVNRouterPort
+	routerIntPortName    openvswitch.OVNRouterPort
+	routerExtPortIPv4Net string
+	routerExtPortIPv6Net string
+	routerIntPortIPv4Net string
+	routerIntPortIPv6Net string
+	routerExtGwIPv4      string
+	routerExtGwIPv6      string
+	routerMAC            net.HardwareAddr
+
+	// External Switch.
+	extSwitchName             openvswitch.OVNSwitch
+	extSwitchRouterPortName   openvswitch.OVNSwitchPort
+	extSwitchProviderPortName openvswitch.OVNSwitchPort
+	extSwitchProviderName     string
+
+	// Internal Switch.
+	intSwitchName               openvswitch.OVNSwitch
+	intSwitchRouterPortName     openvswitch.OVNSwitchPort
+	intSwitchInstancePortPrefix string
+
+	// DNS.
+	domainName    string
+	dnsSearchList []string
+	dnsIPv6       string
+	dnsIPv4       string
+}
+
+// ovn represents a LXD OVN network.
+type ovn struct {
+	common
+}
+
+// getOVNVars returns OVN object variables generated from current config.
+func (n *ovn) getOVNVars() (*ovnVars, error) {
+	var err error
+	v := ovnVars{}
+
+	v.client = openvswitch.NewOVN()
+	v.client.SetDatabaseAddress("tcp:10.109.89.169:6643") // tomp TODO we need to get this out of host OVS or LXD daemon config.
+
+	// Initialise network object names.
+	netName := fmt.Sprintf("lxd-net%d", n.id)
+
+	// Chassis group.
+	v.chassisGroupName = openvswitch.OVNChassisGroup(netName)
+
+	// Router network objects (for connecting to both external and internal switches).
+	v.routerName = openvswitch.OVNRouter(fmt.Sprintf("%s-lr", netName))
+	v.routerExtPortName = openvswitch.OVNRouterPort(fmt.Sprintf("%s-lrp-ext", v.routerName))
+	v.routerIntPortName = openvswitch.OVNRouterPort(fmt.Sprintf("%s-lrp-int", v.routerName))
+	routerMAC := n.config["bridge.hwaddr"]
+	if routerMAC == "" {
+		routerMAC = n.config["volatile.bridge.hwaddr"]
+	}
+
+	v.routerMAC, err = net.ParseMAC(routerMAC)
+	if err != nil {
+		return nil, errors.Wrapf(err, "Failed parsing router MAC address %q", routerMAC)
+	}
+
+	// Load parent network.
+	parentNet, err := LoadByName(n.state, n.config["parent"])
+	if err != nil {
+		return nil, errors.Wrapf(err, "Failead loading parent network")
+	}
+	parentNetConf := parentNet.Config()
+
+	// Parent derived settings.
+	v.extSwitchProviderName = parentNet.Name()
+
+	parentIPv4, parentIPv4Mask, err := net.ParseCIDR(parentNetConf["ipv4.address"])
+	if err == nil {
+		v.dnsIPv4 = parentIPv4.String()
+		v.routerExtGwIPv4 = parentIPv4.String()
+	}
+
+	parentIPv6, parentIPv6Mask, err := net.ParseCIDR(parentNetConf["ipv6.address"])
+	if err == nil {
+		v.dnsIPv6 = parentIPv6.String()
+		v.routerExtGwIPv6 = parentIPv6.String()
+	}
+
+	// Allocate (or retrieve existing allocation) for the router's external IP on the parent network.
+	opts := &dhcpalloc.Options{
+		ProjectName: project.Default,
+		HostName:    netName,
+		HostMAC:     v.routerMAC,
+		Network:     parentNet,
+	}
+
+	err = dhcpalloc.AllocateTask(opts, func(t *dhcpalloc.Transaction) error {
+		if parentIPv4 != nil {
+			routerExtPortIPv4, err := t.AllocateIPv4()
+
+			// If DHCP not supported, skip error, and will result in total protocol filter.
+			if err != nil && err != dhcpalloc.ErrDHCPNotSupported {
+				return err
+			}
+
+			routerExtPortIPv4Net := &net.IPNet{
+				Mask: parentIPv4Mask.Mask,
+				IP:   routerExtPortIPv4,
+			}
+			v.routerExtPortIPv4Net = routerExtPortIPv4Net.String()
+		}
+
+		if parentIPv6 != nil {
+			routerExtPortIPv6, err := t.AllocateIPv6()
+
+			// If DHCP not supported, skip error, and will result in total protocol filter.
+			if err != nil && err != dhcpalloc.ErrDHCPNotSupported {
+				return err
+			}
+
+			routerExtPortIPv6Net := &net.IPNet{
+				Mask: parentIPv6Mask.Mask,
+				IP:   routerExtPortIPv6,
+			}
+			v.routerExtPortIPv6Net = routerExtPortIPv6Net.String()
+		}
+
+		return nil
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	// Router settings.
+	v.routerIntPortIPv4Net = n.config["ipv4.address"]
+	v.routerIntPortIPv6Net = n.config["ipv6.address"]
+
+	// Domain settings.
+	v.domainName = "lxd"
+	if n.config["dns.domain"] != "" {
+		v.domainName = n.config["dns.domain"]
+	}
+
+	v.dnsSearchList = []string{"dns.domain"}
+	if n.config["dns.search"] != "" {
+		v.dnsSearchList = []string{}
+		for _, domain := range strings.SplitN(n.config["dns.search"], ",", -1) {
+			v.dnsSearchList = append(v.dnsSearchList, strings.TrimSpace(domain))
+		}
+	}
+
+	// External network objects (for connecting logical router and external network).
+	v.extSwitchName = openvswitch.OVNSwitch(fmt.Sprintf("%s-ls-ext", netName))
+	v.extSwitchRouterPortName = openvswitch.OVNSwitchPort(fmt.Sprintf("%s-lsp-router", v.extSwitchName))
+	v.extSwitchProviderPortName = openvswitch.OVNSwitchPort(fmt.Sprintf("%s-lsp-provider", v.extSwitchName))
+
+	// Internal network objects (for connecting logical router and instance NICs).
+	v.intSwitchName = openvswitch.OVNSwitch(fmt.Sprintf("%s-ls-int", netName))
+	v.intSwitchRouterPortName = openvswitch.OVNSwitchPort(fmt.Sprintf("%s-lsp-router", v.intSwitchName))
+	v.intSwitchInstancePortPrefix = fmt.Sprintf("%s-instance", netName)
+
+	return &v, nil
+}
+
+// Validate network config.
+func (n *ovn) Validate(config map[string]string) error {
+	rules := map[string]func(value string) error{
+		"parent": func(value string) error {
+			if err := ValidNetworkName(value); err != nil {
+				return errors.Wrapf(err, "Invalid network name %q", value)
+			}
+
+			return nil
+		},
+		"maas.subnet.ipv4": validate.IsAny,
+		"maas.subnet.ipv6": validate.IsAny,
+		"bridge.hwaddr": func(value string) error {
+			if value == "" {
+				return nil
+			}
+
+			return validate.IsNetworkMAC(value)
+		},
+		"volatile.bridge.hwaddr": func(value string) error {
+			if value == "" {
+				return nil
+			}
+
+			return validate.IsNetworkMAC(value)
+		},
+		"ipv4.address": func(value string) error {
+			if validate.IsOneOf(value, []string{"auto"}) == nil {
+				return nil
+			}
+
+			return validate.Optional(validate.IsNetworkAddressCIDRV4)(value)
+		},
+		"ipv6.address": func(value string) error {
+			if validate.IsOneOf(value, []string{"auto"}) == nil {
+				return nil
+			}
+
+			return validate.IsNetworkAddressCIDRV6(value)
+		},
+		"dns.domain": validate.IsAny,
+		"dns.search": validate.IsAny,
+	}
+
+	err := n.validate(config, rules)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// fillConfig fills requested config with any default values.
+func (n *ovn) fillConfig(config map[string]string) error {
+	if config["ipv4.address"] == "" {
+		config["ipv4.address"] = "auto"
+	}
+
+	if config["ipv6.address"] == "" {
+		content, err := ioutil.ReadFile("/proc/sys/net/ipv6/conf/default/disable_ipv6")
+		if err == nil && string(content) == "0\n" {
+			config["ipv6.address"] = "auto"
+		}
+	}
+
+	// If no static hwaddr specified generate a volatile one to store in DB record so that
+	// there are no races when starting the network at the same time on multiple cluster nodes.
+	err := n.fillHwaddr(config)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// fillHwaddr populates the volatile.bridge.hwaddr in config if it, nor bridge.hwaddr, are already set.
+func (n *ovn) fillHwaddr(config map[string]string) error {
+	if config["bridge.hwaddr"] != "" || config["volatile.bridge.hwaddr"] != "" {
+		return nil
+	}
+
+	// If no existing MAC address, generate a new one and store in volatile.
+	hwAddr, err := instance.DeviceNextInterfaceHWAddr()
+	if err != nil {
+		return errors.Wrapf(err, "Failed generating MAC address")
+	}
+
+	config["volatile.bridge.hwaddr"] = hwAddr
+	return nil
+}
+
+// Create sets up network in OVN Northbound database.
+func (n *ovn) Create(clusterNotification bool) error {
+	n.logger.Debug("Create", log.Ctx{"clusterNotification": clusterNotification, "config": n.config})
+
+	// We only need to setup the OVN Northbound database once, not on every clustered node.
+	if !clusterNotification {
+		err := n.setup(false)
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func (n *ovn) setup(update bool) error {
+	// If we are in mock mode, just no-op.
+	if n.state.OS.MockMode {
+		return nil
+	}
+
+	n.logger.Debug("Setting up network")
+
+	revert := revert.New()
+	defer revert.Fail()
+
+	var routerExtPortIPv4, routerIntPortIPv4, routerExtPortIPv6, routerIntPortIPv6 net.IP
+	var routerExtPortIPv4Net, routerIntPortIPv4Net, routerExtPortIPv6Net, routerIntPortIPv6Net *net.IPNet
+
+	ovn, err := n.getOVNVars()
+	if err != nil {
+		return err
+	}
+
+	// Parse router IP config.
+	if ovn.routerExtPortIPv4Net != "" {
+		routerExtPortIPv4, routerExtPortIPv4Net, err = net.ParseCIDR(ovn.routerExtPortIPv4Net)
+		if err != nil {
+			return err
+		}
+	}
+
+	if ovn.routerExtPortIPv6Net != "" {
+		routerExtPortIPv6, routerExtPortIPv6Net, err = net.ParseCIDR(ovn.routerExtPortIPv6Net)
+		if err != nil {
+			return err
+		}
+	}
+
+	if ovn.routerIntPortIPv4Net != "" {
+		routerIntPortIPv4, routerIntPortIPv4Net, err = net.ParseCIDR(ovn.routerIntPortIPv4Net)
+		if err != nil {
+			return err
+		}
+	}
+
+	if ovn.routerIntPortIPv6Net != "" {
+		routerIntPortIPv6, routerIntPortIPv6Net, err = net.ParseCIDR(ovn.routerIntPortIPv6Net)
+		if err != nil {
+			return err
+		}
+	}
+
+	// Create chassis group.
+	err = ovn.client.ChassisGroupAdd(ovn.chassisGroupName, update)
+	if err != nil {
+		return err
+	}
+
+	revert.Add(func() { ovn.client.ChassisGroupDelete(ovn.chassisGroupName) })
+
+	// Create logical router.
+	if update {
+		ovn.client.LogicalRouterDelete(ovn.routerName)
+	}
+
+	err = ovn.client.LogicalRouterAdd(ovn.routerName)
+	if err != nil {
+		return errors.Wrapf(err, "Failed adding router")
+	}
+	revert.Add(func() { ovn.client.LogicalRouterDelete(ovn.routerName) })
+
+	// Configure logical router.
+
+	// Add default routes.
+	if ovn.routerExtGwIPv4 != "" {
+		err = ovn.client.LogicalRouterRouteAdd(ovn.routerName, &net.IPNet{IP: net.IPv4zero, Mask: net.CIDRMask(0, 32)}, net.ParseIP(ovn.routerExtGwIPv4))
+		if err != nil {
+			return errors.Wrapf(err, "Failed adding IPv4 default route")
+		}
+	}
+
+	if ovn.routerExtGwIPv6 != "" {
+		err = ovn.client.LogicalRouterRouteAdd(ovn.routerName, &net.IPNet{IP: net.IPv6zero, Mask: net.CIDRMask(0, 128)}, net.ParseIP(ovn.routerExtGwIPv6))
+		if err != nil {
+			return errors.Wrapf(err, "Failed adding IPv6 default route")
+		}
+	}
+
+	// Add SNAT rules.
+	if routerIntPortIPv4Net != nil {
+		err = ovn.client.LogicalRouterSNATAdd(ovn.routerName, routerIntPortIPv4Net, routerExtPortIPv4)
+		if err != nil {
+			return err
+		}
+	}
+
+	if routerIntPortIPv6Net != nil {
+		err = ovn.client.LogicalRouterSNATAdd(ovn.routerName, routerIntPortIPv6Net, routerExtPortIPv6)
+		if err != nil {
+			return err
+		}
+	}
+
+	// Create external logical switch.
+	if update {
+		ovn.client.LogicalSwitchDelete(ovn.extSwitchName)
+	}
+
+	err = ovn.client.LogicalSwitchAdd(ovn.extSwitchName, false)
+	if err != nil {
+		return errors.Wrapf(err, "Failed adding external switch")
+	}
+	revert.Add(func() { ovn.client.LogicalSwitchDelete(ovn.extSwitchName) })
+
+	// Generate external router port IPs (in CIDR format).
+	extRouterIPs := []*net.IPNet{}
+	if routerExtPortIPv4Net != nil {
+		extRouterIPs = append(extRouterIPs, &net.IPNet{
+			IP:   routerExtPortIPv4,
+			Mask: routerExtPortIPv4Net.Mask,
+		})
+	}
+
+	if routerExtPortIPv6Net != nil {
+		extRouterIPs = append(extRouterIPs, &net.IPNet{
+			IP:   routerExtPortIPv6,
+			Mask: routerExtPortIPv6Net.Mask,
+		})
+	}
+
+	// Create external router port.
+	err = ovn.client.LogicalRouterPortAdd(ovn.routerName, ovn.routerExtPortName, ovn.routerMAC, extRouterIPs...)
+	if err != nil {
+		return errors.Wrapf(err, "Failed adding external router port")
+	}
+	revert.Add(func() { ovn.client.LogicalRouterPortDelete(ovn.routerExtPortName) })
+
+	// Create external switch port and link to router port.
+	err = ovn.client.LogicalSwitchPortAdd(ovn.extSwitchName, ovn.extSwitchRouterPortName, false)
+	if err != nil {
+		return errors.Wrapf(err, "Failed adding external switch router port")
+	}
+	revert.Add(func() { ovn.client.LogicalSwitchPortDelete(ovn.extSwitchRouterPortName) })
+
+	err = ovn.client.LogicalSwitchPortLinkRouter(ovn.extSwitchRouterPortName, ovn.routerExtPortName)
+	if err != nil {
+		return errors.Wrapf(err, "Failed linking external router port to external switch port")
+	}
+
+	// Create external switch port and link to external provider network.
+	err = ovn.client.LogicalSwitchPortAdd(ovn.extSwitchName, ovn.extSwitchProviderPortName, false)
+	if err != nil {
+		return errors.Wrapf(err, "Failed adding external switch provider port")
+	}
+	revert.Add(func() { ovn.client.LogicalSwitchPortDelete(ovn.extSwitchProviderPortName) })
+
+	err = ovn.client.LogicalSwitchPortLinkProviderNetwork(ovn.extSwitchProviderPortName, ovn.extSwitchProviderName)
+	if err != nil {
+		return errors.Wrapf(err, "Failed linking external switch provider port to external provider network")
+	}
+
+	// Create internal logical switch if not updating.
+	err = ovn.client.LogicalSwitchAdd(ovn.intSwitchName, update)
+	if err != nil {
+		return errors.Wrapf(err, "Failed adding internal switch")
+	}
+	revert.Add(func() { ovn.client.LogicalSwitchDelete(ovn.intSwitchName) })
+
+	// Setup IP allocation config on logical switch.
+	err = ovn.client.LogicalSwitchSetIPAllocation(ovn.intSwitchName, &openvswitch.OVNIPAllocationOpts{
+		PrefixIPv4:  routerIntPortIPv4Net,
+		PrefixIPv6:  routerIntPortIPv6Net,
+		ExcludeIPv4: []openvswitch.OVNIPRange{{Start: routerIntPortIPv4}},
+	})
+	if err != nil {
+		return errors.Wrapf(err, "Failed setting IP allocation settings on internal switch")
+	}
+
+	// Find MAC address for internal router port.
+
+	var dhcpv4UUID, dhcpv6UUID string
+
+	if update {
+		// Find first existing DHCP options set for IPv4 and IPv6 and update them instead of adding sets.
+		existingOpts, err := ovn.client.LogicalSwitchDHCPOptionsGet(ovn.intSwitchName)
+		if err != nil {
+			return errors.Wrapf(err, "Failed getting existing DHCP settings for internal switch")
+		}
+
+		for _, existingOpt := range existingOpts {
+			if existingOpt.CIDR.IP.To4() == nil {
+				if dhcpv6UUID == "" {
+					dhcpv6UUID = existingOpt.UUID
+				}
+			} else {
+				if dhcpv4UUID == "" {
+					dhcpv4UUID = existingOpt.UUID
+				}
+			}
+		}
+	}
+
+	// Create DHCPv4 options for internal switch.
+	err = ovn.client.LogicalSwitchDHCPv4OptionsSet(ovn.intSwitchName, dhcpv4UUID, routerIntPortIPv4Net, &openvswitch.OVNDHCPv4Opts{
+		ServerID:           routerIntPortIPv4,
+		ServerMAC:          ovn.routerMAC,
+		Router:             routerIntPortIPv4,
+		RecursiveDNSServer: net.ParseIP(ovn.dnsIPv4),
+		DomainName:         ovn.domainName,
+		LeaseTime:          time.Duration(time.Hour * 1),
+	})
+	if err != nil {
+		return errors.Wrapf(err, "Failed adding DHCPv4 settings for internal switch")
+	}
+
+	// Create DHCPv6 options for internal switch.
+	err = ovn.client.LogicalSwitchDHCPv6OptionsSet(ovn.intSwitchName, dhcpv6UUID, routerIntPortIPv6Net, &openvswitch.OVNDHCPv6Opts{
+		ServerID:           ovn.routerMAC,
+		RecursiveDNSServer: net.ParseIP(ovn.dnsIPv6),
+		DNSSearchList:      ovn.dnsSearchList,
+	})
+	if err != nil {
+		return errors.Wrapf(err, "Failed adding DHCPv6 settings for internal switch")
+	}
+
+	// Generate internal router port IPs (in CIDR format).
+	intRouterIPs := []*net.IPNet{}
+	if routerIntPortIPv4Net != nil {
+		intRouterIPs = append(intRouterIPs, &net.IPNet{
+			IP:   routerIntPortIPv4,
+			Mask: routerIntPortIPv4Net.Mask,
+		})
+	}
+
+	if routerIntPortIPv6Net != nil {
+		intRouterIPs = append(intRouterIPs, &net.IPNet{
+			IP:   routerIntPortIPv6,
+			Mask: routerIntPortIPv6Net.Mask,
+		})
+	}
+
+	// Create internal router port.
+	err = ovn.client.LogicalRouterPortAdd(ovn.routerName, ovn.routerIntPortName, ovn.routerMAC, intRouterIPs...)
+	if err != nil {
+		return errors.Wrapf(err, "Failed adding internal router port")
+	}
+	revert.Add(func() { ovn.client.LogicalRouterPortDelete(ovn.routerIntPortName) })
+
+	// Set IPv6 router advertisement settings.
+	if routerIntPortIPv6Net != nil {
+		err = ovn.client.LogicalRouterPortSetIPv6Advertisements(ovn.routerIntPortName, &openvswitch.OVNIPv6RAOpts{
+			AddressMode:        openvswitch.OVNIPv6AddressModeSLAAC,
+			SendPeriodic:       true,
+			DNSSearchList:      ovn.dnsSearchList,
+			RecursiveDNSServer: net.ParseIP(ovn.dnsIPv6),
+
+			// Keep these low until we support DNS search domains via DHCPv4, as otherwise RA DNSSL
+			// won't take effect until advert after DHCPv4 has run on instance.
+			MinInterval: time.Duration(time.Second * 30),
+			MaxInterval: time.Duration(time.Minute * 1),
+		})
+		if err != nil {
+			return errors.Wrapf(err, "Failed setting internal router port IPv6 advertisement settings")
+		}
+	}
+
+	// Create internal switch port and link to router port.
+	err = ovn.client.LogicalSwitchPortAdd(ovn.intSwitchName, ovn.intSwitchRouterPortName, update)
+	if err != nil {
+		return errors.Wrapf(err, "Failed adding internal switch router port")
+	}
+	revert.Add(func() { ovn.client.LogicalSwitchPortDelete(ovn.intSwitchRouterPortName) })
+
+	err = ovn.client.LogicalSwitchPortLinkRouter(ovn.intSwitchRouterPortName, ovn.routerIntPortName)
+	if err != nil {
+		return errors.Wrapf(err, "Failed linking internal router port to internal switch port")
+	}
+
+	revert.Success()
+	return nil
+}
+
+// Delete deletes a network.
+func (n *ovn) Delete(clusterNotification bool) error {
+	n.logger.Debug("Delete", log.Ctx{"clusterNotification": clusterNotification})
+
+	if !clusterNotification {
+		ovn, err := n.getOVNVars()
+		if err != nil {
+			return err
+		}
+
+		ovn.client.ChassisGroupDelete(ovn.chassisGroupName)
+		ovn.client.LogicalRouterDelete(ovn.routerName)
+		ovn.client.LogicalSwitchDelete(ovn.extSwitchName)
+		ovn.client.LogicalSwitchDelete(ovn.intSwitchName)
+		ovn.client.LogicalRouterPortDelete(ovn.routerExtPortName)
+		ovn.client.LogicalRouterPortDelete(ovn.routerIntPortName)
+		ovn.client.LogicalSwitchPortDelete(ovn.extSwitchRouterPortName)
+		ovn.client.LogicalSwitchPortDelete(ovn.extSwitchProviderPortName)
+		ovn.client.LogicalSwitchPortDelete(ovn.intSwitchRouterPortName)
+	}
+
+	return n.common.delete(clusterNotification)
+}
+
+// Rename renames a network.
+func (n *ovn) Rename(newName string) error {
+	n.logger.Debug("Rename", log.Ctx{"newName": newName})
+
+	// Sanity checks.
+	inUse, err := n.IsUsed()
+	if err != nil {
+		return err
+	}
+
+	if inUse {
+		return fmt.Errorf("The network is currently in use")
+	}
+
+	// Rename common steps.
+	err = n.common.rename(newName)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// Start starts is a no-op.
+func (n *ovn) Start() error {
+	if n.status == api.NetworkStatusPending {
+		return fmt.Errorf("Cannot start pending network")
+	}
+
+	return nil
+}
+
+// Stop stops is a no-op.
+func (n *ovn) Stop() error {
+	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 *ovn) Update(newNetwork api.NetworkPut, targetNode string, clusterNotification bool) error {
+	n.logger.Debug("Update", log.Ctx{"clusterNotification": clusterNotification, "newNetwork": newNetwork})
+
+	// Populate default values if they are missing.
+	err := n.fillConfig(newNetwork.Config)
+	if err != nil {
+		return err
+	}
+
+	// Populate auto fields.
+	err = fillAuto(newNetwork.Config)
+	if err != nil {
+		return err
+	}
+
+	dbUpdateNeeeded, changedKeys, 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, clusterNotification)
+
+		// Reset any change that was made to network.
+		if !clusterNotification {
+			n.setup(true)
+		}
+	})
+
+	// Apply changes to database.
+	err = n.common.update(newNetwork, targetNode, clusterNotification)
+	if err != nil {
+		return err
+	}
+
+	// Restart the network if needed.
+	if len(changedKeys) > 0 && !clusterNotification {
+		err = n.setup(true)
+		if err != nil {
+			return err
+		}
+	}
+
+	revert.Success()
+	return nil
+}
+
+// getInstanceDevicePortName returns the switch port name to use for an instance device.
+func (n *ovn) getInstanceDevicePortName(v *ovnVars, instanceID int, deviceName string) openvswitch.OVNSwitchPort {
+	return openvswitch.OVNSwitchPort(fmt.Sprintf("%s-%d-%s", v.intSwitchInstancePortPrefix, instanceID, deviceName))
+}
+
+// instanceDevicePortAdd adds an instance device port to the internal logical switch and returns the port name.
+func (n *ovn) instanceDevicePortAdd(instanceID int, deviceName string, mac net.HardwareAddr, ips []net.IP) (openvswitch.OVNSwitchPort, error) {
+	var dhcpV4ID, dhcpv6ID string
+
+	revert := revert.New()
+	defer revert.Fail()
+
+	ovn, err := n.getOVNVars()
+	if err != nil {
+		return "", err
+	}
+
+	// Get DHCP options IDs.
+	if ovn.routerIntPortIPv4Net != "" {
+		_, routerIntPortIPv4Net, err := net.ParseCIDR(ovn.routerIntPortIPv4Net)
+		if err != nil {
+			return "", err
+		}
+
+		dhcpV4ID, err = ovn.client.LogicalSwitchDHCPOptionsGetID(ovn.intSwitchName, routerIntPortIPv4Net)
+		if err != nil {
+			return "", err
+		}
+	}
+
+	if ovn.routerIntPortIPv6Net != "" {
+		_, routerIntPortIPv6Net, err := net.ParseCIDR(ovn.routerIntPortIPv6Net)
+		if err != nil {
+			return "", err
+		}
+
+		dhcpv6ID, err = ovn.client.LogicalSwitchDHCPOptionsGetID(ovn.intSwitchName, routerIntPortIPv6Net)
+		if err != nil {
+			return "", err
+		}
+	}
+
+	instancePortName := n.getInstanceDevicePortName(ovn, instanceID, deviceName)
+
+	// Add port with mayExist set to true, so that if instance port exists, we don't fail and continue below
+	// to configure the port as needed. This is required in case the OVN northbound database was unavailable
+	// when the instance NIC was stopped and was unable to remove the port on last stop, which would otherwise
+	// prevent future NIC starts.
+	err = ovn.client.LogicalSwitchPortAdd(ovn.intSwitchName, instancePortName, true)
+	if err != nil {
+		return "", err
+	}
+
+	revert.Add(func() { ovn.client.LogicalSwitchPortDelete(instancePortName) })
+
+	err = ovn.client.LogicalSwitchPortSet(instancePortName, &openvswitch.OVNSwitchPortOpts{
+		DHCPv4OptsID: dhcpV4ID,
+		DHCPv6OptsID: dhcpv6ID,
+		MAC:          mac,
+		IPs:          ips,
+	})
+	if err != nil {
+		return "", err
+	}
+
+	revert.Success()
+	return instancePortName, nil
+}
+
+// instanceDevicePortDelete deletes an instance device port from the internal logical switch.
+func (n *ovn) instanceDevicePortDelete(instanceID int, deviceName string) error {
+	ovn, err := n.getOVNVars()
+	if err != nil {
+		return err
+	}
+
+	instancePortName := n.getInstanceDevicePortName(ovn, instanceID, deviceName)
+
+	err = ovn.client.LogicalSwitchPortDelete(instancePortName)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}

From 4e3017ca427543389c673d55b943a78868680b0b Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Mon, 3 Aug 2020 13:46:15 +0100
Subject: [PATCH 25/33] shared/validate: Makes IsDeviceID non-optional

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

diff --git a/shared/validate/validate.go b/shared/validate/validate.go
index e4ce08cefa..3c590f37da 100644
--- a/shared/validate/validate.go
+++ b/shared/validate/validate.go
@@ -123,10 +123,6 @@ func IsSize(value string) error {
 
 // IsDeviceID validates string is four lowercase hex characters suitable as Vendor or Device ID.
 func IsDeviceID(value string) error {
-	if value == "" {
-		return nil
-	}
-
 	regexHexLc, err := regexp.Compile("^[0-9a-f]+$")
 	if err != nil {
 		return err

From 39d171809d37de54527588d4b990adde7a85502c Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Mon, 3 Aug 2020 13:46:59 +0100
Subject: [PATCH 26/33] lxd/device: Wraps validate.IsDeviceID in
 validate.Optional

Signed-off-by: Thomas Parrott <thomas.parrott at canonical.com>
---
 lxd/device/gpu.go          | 4 ++--
 lxd/device/unix_hotplug.go | 4 ++--
 lxd/device/usb.go          | 4 ++--
 3 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/lxd/device/gpu.go b/lxd/device/gpu.go
index 420ff0f450..9c7724077b 100644
--- a/lxd/device/gpu.go
+++ b/lxd/device/gpu.go
@@ -40,8 +40,8 @@ func (d *gpu) validateConfig(instConf instance.ConfigReader) error {
 	}
 
 	rules := map[string]func(string) error{
-		"vendorid":  validate.IsDeviceID,
-		"productid": validate.IsDeviceID,
+		"vendorid":  validate.Optional(validate.IsDeviceID),
+		"productid": validate.Optional(validate.IsDeviceID),
 		"id":        validate.IsAny,
 		"pci":       validate.IsAny,
 		"uid":       unixValidUserID,
diff --git a/lxd/device/unix_hotplug.go b/lxd/device/unix_hotplug.go
index 71f62b6f0b..6f3f39370a 100644
--- a/lxd/device/unix_hotplug.go
+++ b/lxd/device/unix_hotplug.go
@@ -48,8 +48,8 @@ func (d *unixHotplug) validateConfig(instConf instance.ConfigReader) error {
 	}
 
 	rules := map[string]func(string) error{
-		"vendorid":  validate.IsDeviceID,
-		"productid": validate.IsDeviceID,
+		"vendorid":  validate.Optional(validate.IsDeviceID),
+		"productid": validate.Optional(validate.IsDeviceID),
 		"uid":       unixValidUserID,
 		"gid":       unixValidUserID,
 		"mode":      unixValidOctalFileMode,
diff --git a/lxd/device/usb.go b/lxd/device/usb.go
index 155e62cec6..6e5c09aab9 100644
--- a/lxd/device/usb.go
+++ b/lxd/device/usb.go
@@ -50,8 +50,8 @@ func (d *usb) validateConfig(instConf instance.ConfigReader) error {
 	}
 
 	rules := map[string]func(string) error{
-		"vendorid":  validate.IsDeviceID,
-		"productid": validate.IsDeviceID,
+		"vendorid":  validate.Optional(validate.IsDeviceID),
+		"productid": validate.Optional(validate.IsDeviceID),
 		"uid":       unixValidUserID,
 		"gid":       unixValidUserID,
 		"mode":      unixValidOctalFileMode,

From 3aaf8dbbca9b5c57933acd30d74e7d6ba6229515 Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Mon, 3 Aug 2020 13:47:28 +0100
Subject: [PATCH 27/33] shared/validate: Makes IsNetworkV6 non-optional

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

diff --git a/shared/validate/validate.go b/shared/validate/validate.go
index 3c590f37da..f7b7da53d2 100644
--- a/shared/validate/validate.go
+++ b/shared/validate/validate.go
@@ -230,10 +230,6 @@ func IsNetworkV4List(value string) error {
 
 // IsNetworkV6 validates an IPv6 CIDR string. If string is empty, returns valid.
 func IsNetworkV6(value string) error {
-	if value == "" {
-		return nil
-	}
-
 	ip, subnet, err := net.ParseCIDR(value)
 	if err != nil {
 		return err

From 12af7aada96ec58583f4abd086f1474607b46211 Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Mon, 3 Aug 2020 13:49:52 +0100
Subject: [PATCH 28/33] shared/validate: Makes IsNetworkAddressCIDRV6
 non-optional

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

diff --git a/shared/validate/validate.go b/shared/validate/validate.go
index f7b7da53d2..0856e89f0d 100644
--- a/shared/validate/validate.go
+++ b/shared/validate/validate.go
@@ -262,10 +262,6 @@ func IsNetworkAddressV6(value string) error {
 
 // IsNetworkAddressCIDRV6 validates an IPv6 addresss string in CIDR format. If string is empty, returns valid.
 func IsNetworkAddressCIDRV6(value string) error {
-	if value == "" {
-		return nil
-	}
-
 	ip, subnet, err := net.ParseCIDR(value)
 	if err != nil {
 		return err

From 75463c23387172cd03fc9a9361cca7d364f7d358 Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Mon, 3 Aug 2020 13:50:55 +0100
Subject: [PATCH 29/33] lxd: Wraps validate.IsNetworkAddressCIDRV6 in
 validate.Optional

Signed-off-by: Thomas Parrott <thomas.parrott at canonical.com>
---
 lxd/main_init_interactive.go |   2 +-
 lxd/network/driver_bridge.go |   2 +-
 lxd/network/driver_ovn.go    | 778 -----------------------------------
 3 files changed, 2 insertions(+), 780 deletions(-)
 delete mode 100644 lxd/network/driver_ovn.go

diff --git a/lxd/main_init_interactive.go b/lxd/main_init_interactive.go
index 4414cb3241..7a6bacdf5a 100644
--- a/lxd/main_init_interactive.go
+++ b/lxd/main_init_interactive.go
@@ -385,7 +385,7 @@ func (c *cmdInit) askNetworking(config *cmdInitData, d lxd.InstanceServer) error
 				return nil
 			}
 
-			return validate.IsNetworkAddressCIDRV6(value)
+			return validate.Optional(validate.IsNetworkAddressCIDRV6)(value)
 		})
 
 		if !shared.StringInSlice(net.Config["ipv6.address"], []string{"auto", "none"}) {
diff --git a/lxd/network/driver_bridge.go b/lxd/network/driver_bridge.go
index 1cb7773279..d96a13acfd 100644
--- a/lxd/network/driver_bridge.go
+++ b/lxd/network/driver_bridge.go
@@ -195,7 +195,7 @@ func (n *bridge) Validate(config map[string]string) error {
 				return nil
 			}
 
-			return validate.IsNetworkAddressCIDRV6(value)
+			return validate.Optional(validate.IsNetworkAddressCIDRV6)(value)
 		},
 		"ipv6.firewall": validate.Optional(validate.IsBool),
 		"ipv6.nat":      validate.Optional(validate.IsBool),
diff --git a/lxd/network/driver_ovn.go b/lxd/network/driver_ovn.go
deleted file mode 100644
index 2da3430e90..0000000000
--- a/lxd/network/driver_ovn.go
+++ /dev/null
@@ -1,778 +0,0 @@
-package network
-
-import (
-	"fmt"
-	"io/ioutil"
-	"net"
-	"strings"
-	"time"
-
-	"github.com/pkg/errors"
-
-	"github.com/lxc/lxd/lxd/dnsmasq/dhcpalloc"
-	"github.com/lxc/lxd/lxd/instance"
-	"github.com/lxc/lxd/lxd/network/openvswitch"
-	"github.com/lxc/lxd/lxd/project"
-	"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"
-)
-
-//ovnVars OVN object variables.
-type ovnVars struct {
-	// OVN client.
-	client *openvswitch.OVN
-
-	// Chassis.
-	chassisGroupName openvswitch.OVNChassisGroup
-
-	// Router.
-	routerName           openvswitch.OVNRouter
-	routerExtPortName    openvswitch.OVNRouterPort
-	routerIntPortName    openvswitch.OVNRouterPort
-	routerExtPortIPv4Net string
-	routerExtPortIPv6Net string
-	routerIntPortIPv4Net string
-	routerIntPortIPv6Net string
-	routerExtGwIPv4      string
-	routerExtGwIPv6      string
-	routerMAC            net.HardwareAddr
-
-	// External Switch.
-	extSwitchName             openvswitch.OVNSwitch
-	extSwitchRouterPortName   openvswitch.OVNSwitchPort
-	extSwitchProviderPortName openvswitch.OVNSwitchPort
-	extSwitchProviderName     string
-
-	// Internal Switch.
-	intSwitchName               openvswitch.OVNSwitch
-	intSwitchRouterPortName     openvswitch.OVNSwitchPort
-	intSwitchInstancePortPrefix string
-
-	// DNS.
-	domainName    string
-	dnsSearchList []string
-	dnsIPv6       string
-	dnsIPv4       string
-}
-
-// ovn represents a LXD OVN network.
-type ovn struct {
-	common
-}
-
-// getOVNVars returns OVN object variables generated from current config.
-func (n *ovn) getOVNVars() (*ovnVars, error) {
-	var err error
-	v := ovnVars{}
-
-	v.client = openvswitch.NewOVN()
-	v.client.SetDatabaseAddress("tcp:10.109.89.169:6643") // tomp TODO we need to get this out of host OVS or LXD daemon config.
-
-	// Initialise network object names.
-	netName := fmt.Sprintf("lxd-net%d", n.id)
-
-	// Chassis group.
-	v.chassisGroupName = openvswitch.OVNChassisGroup(netName)
-
-	// Router network objects (for connecting to both external and internal switches).
-	v.routerName = openvswitch.OVNRouter(fmt.Sprintf("%s-lr", netName))
-	v.routerExtPortName = openvswitch.OVNRouterPort(fmt.Sprintf("%s-lrp-ext", v.routerName))
-	v.routerIntPortName = openvswitch.OVNRouterPort(fmt.Sprintf("%s-lrp-int", v.routerName))
-	routerMAC := n.config["bridge.hwaddr"]
-	if routerMAC == "" {
-		routerMAC = n.config["volatile.bridge.hwaddr"]
-	}
-
-	v.routerMAC, err = net.ParseMAC(routerMAC)
-	if err != nil {
-		return nil, errors.Wrapf(err, "Failed parsing router MAC address %q", routerMAC)
-	}
-
-	// Load parent network.
-	parentNet, err := LoadByName(n.state, n.config["parent"])
-	if err != nil {
-		return nil, errors.Wrapf(err, "Failead loading parent network")
-	}
-	parentNetConf := parentNet.Config()
-
-	// Parent derived settings.
-	v.extSwitchProviderName = parentNet.Name()
-
-	parentIPv4, parentIPv4Mask, err := net.ParseCIDR(parentNetConf["ipv4.address"])
-	if err == nil {
-		v.dnsIPv4 = parentIPv4.String()
-		v.routerExtGwIPv4 = parentIPv4.String()
-	}
-
-	parentIPv6, parentIPv6Mask, err := net.ParseCIDR(parentNetConf["ipv6.address"])
-	if err == nil {
-		v.dnsIPv6 = parentIPv6.String()
-		v.routerExtGwIPv6 = parentIPv6.String()
-	}
-
-	// Allocate (or retrieve existing allocation) for the router's external IP on the parent network.
-	opts := &dhcpalloc.Options{
-		ProjectName: project.Default,
-		HostName:    netName,
-		HostMAC:     v.routerMAC,
-		Network:     parentNet,
-	}
-
-	err = dhcpalloc.AllocateTask(opts, func(t *dhcpalloc.Transaction) error {
-		if parentIPv4 != nil {
-			routerExtPortIPv4, err := t.AllocateIPv4()
-
-			// If DHCP not supported, skip error, and will result in total protocol filter.
-			if err != nil && err != dhcpalloc.ErrDHCPNotSupported {
-				return err
-			}
-
-			routerExtPortIPv4Net := &net.IPNet{
-				Mask: parentIPv4Mask.Mask,
-				IP:   routerExtPortIPv4,
-			}
-			v.routerExtPortIPv4Net = routerExtPortIPv4Net.String()
-		}
-
-		if parentIPv6 != nil {
-			routerExtPortIPv6, err := t.AllocateIPv6()
-
-			// If DHCP not supported, skip error, and will result in total protocol filter.
-			if err != nil && err != dhcpalloc.ErrDHCPNotSupported {
-				return err
-			}
-
-			routerExtPortIPv6Net := &net.IPNet{
-				Mask: parentIPv6Mask.Mask,
-				IP:   routerExtPortIPv6,
-			}
-			v.routerExtPortIPv6Net = routerExtPortIPv6Net.String()
-		}
-
-		return nil
-	})
-	if err != nil {
-		return nil, err
-	}
-
-	// Router settings.
-	v.routerIntPortIPv4Net = n.config["ipv4.address"]
-	v.routerIntPortIPv6Net = n.config["ipv6.address"]
-
-	// Domain settings.
-	v.domainName = "lxd"
-	if n.config["dns.domain"] != "" {
-		v.domainName = n.config["dns.domain"]
-	}
-
-	v.dnsSearchList = []string{"dns.domain"}
-	if n.config["dns.search"] != "" {
-		v.dnsSearchList = []string{}
-		for _, domain := range strings.SplitN(n.config["dns.search"], ",", -1) {
-			v.dnsSearchList = append(v.dnsSearchList, strings.TrimSpace(domain))
-		}
-	}
-
-	// External network objects (for connecting logical router and external network).
-	v.extSwitchName = openvswitch.OVNSwitch(fmt.Sprintf("%s-ls-ext", netName))
-	v.extSwitchRouterPortName = openvswitch.OVNSwitchPort(fmt.Sprintf("%s-lsp-router", v.extSwitchName))
-	v.extSwitchProviderPortName = openvswitch.OVNSwitchPort(fmt.Sprintf("%s-lsp-provider", v.extSwitchName))
-
-	// Internal network objects (for connecting logical router and instance NICs).
-	v.intSwitchName = openvswitch.OVNSwitch(fmt.Sprintf("%s-ls-int", netName))
-	v.intSwitchRouterPortName = openvswitch.OVNSwitchPort(fmt.Sprintf("%s-lsp-router", v.intSwitchName))
-	v.intSwitchInstancePortPrefix = fmt.Sprintf("%s-instance", netName)
-
-	return &v, nil
-}
-
-// Validate network config.
-func (n *ovn) Validate(config map[string]string) error {
-	rules := map[string]func(value string) error{
-		"parent": func(value string) error {
-			if err := ValidNetworkName(value); err != nil {
-				return errors.Wrapf(err, "Invalid network name %q", value)
-			}
-
-			return nil
-		},
-		"maas.subnet.ipv4": validate.IsAny,
-		"maas.subnet.ipv6": validate.IsAny,
-		"bridge.hwaddr": func(value string) error {
-			if value == "" {
-				return nil
-			}
-
-			return validate.IsNetworkMAC(value)
-		},
-		"volatile.bridge.hwaddr": func(value string) error {
-			if value == "" {
-				return nil
-			}
-
-			return validate.IsNetworkMAC(value)
-		},
-		"ipv4.address": func(value string) error {
-			if validate.IsOneOf(value, []string{"auto"}) == nil {
-				return nil
-			}
-
-			return validate.Optional(validate.IsNetworkAddressCIDRV4)(value)
-		},
-		"ipv6.address": func(value string) error {
-			if validate.IsOneOf(value, []string{"auto"}) == nil {
-				return nil
-			}
-
-			return validate.IsNetworkAddressCIDRV6(value)
-		},
-		"dns.domain": validate.IsAny,
-		"dns.search": validate.IsAny,
-	}
-
-	err := n.validate(config, rules)
-	if err != nil {
-		return err
-	}
-
-	return nil
-}
-
-// fillConfig fills requested config with any default values.
-func (n *ovn) fillConfig(config map[string]string) error {
-	if config["ipv4.address"] == "" {
-		config["ipv4.address"] = "auto"
-	}
-
-	if config["ipv6.address"] == "" {
-		content, err := ioutil.ReadFile("/proc/sys/net/ipv6/conf/default/disable_ipv6")
-		if err == nil && string(content) == "0\n" {
-			config["ipv6.address"] = "auto"
-		}
-	}
-
-	// If no static hwaddr specified generate a volatile one to store in DB record so that
-	// there are no races when starting the network at the same time on multiple cluster nodes.
-	err := n.fillHwaddr(config)
-	if err != nil {
-		return err
-	}
-
-	return nil
-}
-
-// fillHwaddr populates the volatile.bridge.hwaddr in config if it, nor bridge.hwaddr, are already set.
-func (n *ovn) fillHwaddr(config map[string]string) error {
-	if config["bridge.hwaddr"] != "" || config["volatile.bridge.hwaddr"] != "" {
-		return nil
-	}
-
-	// If no existing MAC address, generate a new one and store in volatile.
-	hwAddr, err := instance.DeviceNextInterfaceHWAddr()
-	if err != nil {
-		return errors.Wrapf(err, "Failed generating MAC address")
-	}
-
-	config["volatile.bridge.hwaddr"] = hwAddr
-	return nil
-}
-
-// Create sets up network in OVN Northbound database.
-func (n *ovn) Create(clusterNotification bool) error {
-	n.logger.Debug("Create", log.Ctx{"clusterNotification": clusterNotification, "config": n.config})
-
-	// We only need to setup the OVN Northbound database once, not on every clustered node.
-	if !clusterNotification {
-		err := n.setup(false)
-		if err != nil {
-			return err
-		}
-	}
-
-	return nil
-}
-
-func (n *ovn) setup(update bool) error {
-	// If we are in mock mode, just no-op.
-	if n.state.OS.MockMode {
-		return nil
-	}
-
-	n.logger.Debug("Setting up network")
-
-	revert := revert.New()
-	defer revert.Fail()
-
-	var routerExtPortIPv4, routerIntPortIPv4, routerExtPortIPv6, routerIntPortIPv6 net.IP
-	var routerExtPortIPv4Net, routerIntPortIPv4Net, routerExtPortIPv6Net, routerIntPortIPv6Net *net.IPNet
-
-	ovn, err := n.getOVNVars()
-	if err != nil {
-		return err
-	}
-
-	// Parse router IP config.
-	if ovn.routerExtPortIPv4Net != "" {
-		routerExtPortIPv4, routerExtPortIPv4Net, err = net.ParseCIDR(ovn.routerExtPortIPv4Net)
-		if err != nil {
-			return err
-		}
-	}
-
-	if ovn.routerExtPortIPv6Net != "" {
-		routerExtPortIPv6, routerExtPortIPv6Net, err = net.ParseCIDR(ovn.routerExtPortIPv6Net)
-		if err != nil {
-			return err
-		}
-	}
-
-	if ovn.routerIntPortIPv4Net != "" {
-		routerIntPortIPv4, routerIntPortIPv4Net, err = net.ParseCIDR(ovn.routerIntPortIPv4Net)
-		if err != nil {
-			return err
-		}
-	}
-
-	if ovn.routerIntPortIPv6Net != "" {
-		routerIntPortIPv6, routerIntPortIPv6Net, err = net.ParseCIDR(ovn.routerIntPortIPv6Net)
-		if err != nil {
-			return err
-		}
-	}
-
-	// Create chassis group.
-	err = ovn.client.ChassisGroupAdd(ovn.chassisGroupName, update)
-	if err != nil {
-		return err
-	}
-
-	revert.Add(func() { ovn.client.ChassisGroupDelete(ovn.chassisGroupName) })
-
-	// Create logical router.
-	if update {
-		ovn.client.LogicalRouterDelete(ovn.routerName)
-	}
-
-	err = ovn.client.LogicalRouterAdd(ovn.routerName)
-	if err != nil {
-		return errors.Wrapf(err, "Failed adding router")
-	}
-	revert.Add(func() { ovn.client.LogicalRouterDelete(ovn.routerName) })
-
-	// Configure logical router.
-
-	// Add default routes.
-	if ovn.routerExtGwIPv4 != "" {
-		err = ovn.client.LogicalRouterRouteAdd(ovn.routerName, &net.IPNet{IP: net.IPv4zero, Mask: net.CIDRMask(0, 32)}, net.ParseIP(ovn.routerExtGwIPv4))
-		if err != nil {
-			return errors.Wrapf(err, "Failed adding IPv4 default route")
-		}
-	}
-
-	if ovn.routerExtGwIPv6 != "" {
-		err = ovn.client.LogicalRouterRouteAdd(ovn.routerName, &net.IPNet{IP: net.IPv6zero, Mask: net.CIDRMask(0, 128)}, net.ParseIP(ovn.routerExtGwIPv6))
-		if err != nil {
-			return errors.Wrapf(err, "Failed adding IPv6 default route")
-		}
-	}
-
-	// Add SNAT rules.
-	if routerIntPortIPv4Net != nil {
-		err = ovn.client.LogicalRouterSNATAdd(ovn.routerName, routerIntPortIPv4Net, routerExtPortIPv4)
-		if err != nil {
-			return err
-		}
-	}
-
-	if routerIntPortIPv6Net != nil {
-		err = ovn.client.LogicalRouterSNATAdd(ovn.routerName, routerIntPortIPv6Net, routerExtPortIPv6)
-		if err != nil {
-			return err
-		}
-	}
-
-	// Create external logical switch.
-	if update {
-		ovn.client.LogicalSwitchDelete(ovn.extSwitchName)
-	}
-
-	err = ovn.client.LogicalSwitchAdd(ovn.extSwitchName, false)
-	if err != nil {
-		return errors.Wrapf(err, "Failed adding external switch")
-	}
-	revert.Add(func() { ovn.client.LogicalSwitchDelete(ovn.extSwitchName) })
-
-	// Generate external router port IPs (in CIDR format).
-	extRouterIPs := []*net.IPNet{}
-	if routerExtPortIPv4Net != nil {
-		extRouterIPs = append(extRouterIPs, &net.IPNet{
-			IP:   routerExtPortIPv4,
-			Mask: routerExtPortIPv4Net.Mask,
-		})
-	}
-
-	if routerExtPortIPv6Net != nil {
-		extRouterIPs = append(extRouterIPs, &net.IPNet{
-			IP:   routerExtPortIPv6,
-			Mask: routerExtPortIPv6Net.Mask,
-		})
-	}
-
-	// Create external router port.
-	err = ovn.client.LogicalRouterPortAdd(ovn.routerName, ovn.routerExtPortName, ovn.routerMAC, extRouterIPs...)
-	if err != nil {
-		return errors.Wrapf(err, "Failed adding external router port")
-	}
-	revert.Add(func() { ovn.client.LogicalRouterPortDelete(ovn.routerExtPortName) })
-
-	// Create external switch port and link to router port.
-	err = ovn.client.LogicalSwitchPortAdd(ovn.extSwitchName, ovn.extSwitchRouterPortName, false)
-	if err != nil {
-		return errors.Wrapf(err, "Failed adding external switch router port")
-	}
-	revert.Add(func() { ovn.client.LogicalSwitchPortDelete(ovn.extSwitchRouterPortName) })
-
-	err = ovn.client.LogicalSwitchPortLinkRouter(ovn.extSwitchRouterPortName, ovn.routerExtPortName)
-	if err != nil {
-		return errors.Wrapf(err, "Failed linking external router port to external switch port")
-	}
-
-	// Create external switch port and link to external provider network.
-	err = ovn.client.LogicalSwitchPortAdd(ovn.extSwitchName, ovn.extSwitchProviderPortName, false)
-	if err != nil {
-		return errors.Wrapf(err, "Failed adding external switch provider port")
-	}
-	revert.Add(func() { ovn.client.LogicalSwitchPortDelete(ovn.extSwitchProviderPortName) })
-
-	err = ovn.client.LogicalSwitchPortLinkProviderNetwork(ovn.extSwitchProviderPortName, ovn.extSwitchProviderName)
-	if err != nil {
-		return errors.Wrapf(err, "Failed linking external switch provider port to external provider network")
-	}
-
-	// Create internal logical switch if not updating.
-	err = ovn.client.LogicalSwitchAdd(ovn.intSwitchName, update)
-	if err != nil {
-		return errors.Wrapf(err, "Failed adding internal switch")
-	}
-	revert.Add(func() { ovn.client.LogicalSwitchDelete(ovn.intSwitchName) })
-
-	// Setup IP allocation config on logical switch.
-	err = ovn.client.LogicalSwitchSetIPAllocation(ovn.intSwitchName, &openvswitch.OVNIPAllocationOpts{
-		PrefixIPv4:  routerIntPortIPv4Net,
-		PrefixIPv6:  routerIntPortIPv6Net,
-		ExcludeIPv4: []openvswitch.OVNIPRange{{Start: routerIntPortIPv4}},
-	})
-	if err != nil {
-		return errors.Wrapf(err, "Failed setting IP allocation settings on internal switch")
-	}
-
-	// Find MAC address for internal router port.
-
-	var dhcpv4UUID, dhcpv6UUID string
-
-	if update {
-		// Find first existing DHCP options set for IPv4 and IPv6 and update them instead of adding sets.
-		existingOpts, err := ovn.client.LogicalSwitchDHCPOptionsGet(ovn.intSwitchName)
-		if err != nil {
-			return errors.Wrapf(err, "Failed getting existing DHCP settings for internal switch")
-		}
-
-		for _, existingOpt := range existingOpts {
-			if existingOpt.CIDR.IP.To4() == nil {
-				if dhcpv6UUID == "" {
-					dhcpv6UUID = existingOpt.UUID
-				}
-			} else {
-				if dhcpv4UUID == "" {
-					dhcpv4UUID = existingOpt.UUID
-				}
-			}
-		}
-	}
-
-	// Create DHCPv4 options for internal switch.
-	err = ovn.client.LogicalSwitchDHCPv4OptionsSet(ovn.intSwitchName, dhcpv4UUID, routerIntPortIPv4Net, &openvswitch.OVNDHCPv4Opts{
-		ServerID:           routerIntPortIPv4,
-		ServerMAC:          ovn.routerMAC,
-		Router:             routerIntPortIPv4,
-		RecursiveDNSServer: net.ParseIP(ovn.dnsIPv4),
-		DomainName:         ovn.domainName,
-		LeaseTime:          time.Duration(time.Hour * 1),
-	})
-	if err != nil {
-		return errors.Wrapf(err, "Failed adding DHCPv4 settings for internal switch")
-	}
-
-	// Create DHCPv6 options for internal switch.
-	err = ovn.client.LogicalSwitchDHCPv6OptionsSet(ovn.intSwitchName, dhcpv6UUID, routerIntPortIPv6Net, &openvswitch.OVNDHCPv6Opts{
-		ServerID:           ovn.routerMAC,
-		RecursiveDNSServer: net.ParseIP(ovn.dnsIPv6),
-		DNSSearchList:      ovn.dnsSearchList,
-	})
-	if err != nil {
-		return errors.Wrapf(err, "Failed adding DHCPv6 settings for internal switch")
-	}
-
-	// Generate internal router port IPs (in CIDR format).
-	intRouterIPs := []*net.IPNet{}
-	if routerIntPortIPv4Net != nil {
-		intRouterIPs = append(intRouterIPs, &net.IPNet{
-			IP:   routerIntPortIPv4,
-			Mask: routerIntPortIPv4Net.Mask,
-		})
-	}
-
-	if routerIntPortIPv6Net != nil {
-		intRouterIPs = append(intRouterIPs, &net.IPNet{
-			IP:   routerIntPortIPv6,
-			Mask: routerIntPortIPv6Net.Mask,
-		})
-	}
-
-	// Create internal router port.
-	err = ovn.client.LogicalRouterPortAdd(ovn.routerName, ovn.routerIntPortName, ovn.routerMAC, intRouterIPs...)
-	if err != nil {
-		return errors.Wrapf(err, "Failed adding internal router port")
-	}
-	revert.Add(func() { ovn.client.LogicalRouterPortDelete(ovn.routerIntPortName) })
-
-	// Set IPv6 router advertisement settings.
-	if routerIntPortIPv6Net != nil {
-		err = ovn.client.LogicalRouterPortSetIPv6Advertisements(ovn.routerIntPortName, &openvswitch.OVNIPv6RAOpts{
-			AddressMode:        openvswitch.OVNIPv6AddressModeSLAAC,
-			SendPeriodic:       true,
-			DNSSearchList:      ovn.dnsSearchList,
-			RecursiveDNSServer: net.ParseIP(ovn.dnsIPv6),
-
-			// Keep these low until we support DNS search domains via DHCPv4, as otherwise RA DNSSL
-			// won't take effect until advert after DHCPv4 has run on instance.
-			MinInterval: time.Duration(time.Second * 30),
-			MaxInterval: time.Duration(time.Minute * 1),
-		})
-		if err != nil {
-			return errors.Wrapf(err, "Failed setting internal router port IPv6 advertisement settings")
-		}
-	}
-
-	// Create internal switch port and link to router port.
-	err = ovn.client.LogicalSwitchPortAdd(ovn.intSwitchName, ovn.intSwitchRouterPortName, update)
-	if err != nil {
-		return errors.Wrapf(err, "Failed adding internal switch router port")
-	}
-	revert.Add(func() { ovn.client.LogicalSwitchPortDelete(ovn.intSwitchRouterPortName) })
-
-	err = ovn.client.LogicalSwitchPortLinkRouter(ovn.intSwitchRouterPortName, ovn.routerIntPortName)
-	if err != nil {
-		return errors.Wrapf(err, "Failed linking internal router port to internal switch port")
-	}
-
-	revert.Success()
-	return nil
-}
-
-// Delete deletes a network.
-func (n *ovn) Delete(clusterNotification bool) error {
-	n.logger.Debug("Delete", log.Ctx{"clusterNotification": clusterNotification})
-
-	if !clusterNotification {
-		ovn, err := n.getOVNVars()
-		if err != nil {
-			return err
-		}
-
-		ovn.client.ChassisGroupDelete(ovn.chassisGroupName)
-		ovn.client.LogicalRouterDelete(ovn.routerName)
-		ovn.client.LogicalSwitchDelete(ovn.extSwitchName)
-		ovn.client.LogicalSwitchDelete(ovn.intSwitchName)
-		ovn.client.LogicalRouterPortDelete(ovn.routerExtPortName)
-		ovn.client.LogicalRouterPortDelete(ovn.routerIntPortName)
-		ovn.client.LogicalSwitchPortDelete(ovn.extSwitchRouterPortName)
-		ovn.client.LogicalSwitchPortDelete(ovn.extSwitchProviderPortName)
-		ovn.client.LogicalSwitchPortDelete(ovn.intSwitchRouterPortName)
-	}
-
-	return n.common.delete(clusterNotification)
-}
-
-// Rename renames a network.
-func (n *ovn) Rename(newName string) error {
-	n.logger.Debug("Rename", log.Ctx{"newName": newName})
-
-	// Sanity checks.
-	inUse, err := n.IsUsed()
-	if err != nil {
-		return err
-	}
-
-	if inUse {
-		return fmt.Errorf("The network is currently in use")
-	}
-
-	// Rename common steps.
-	err = n.common.rename(newName)
-	if err != nil {
-		return err
-	}
-
-	return nil
-}
-
-// Start starts is a no-op.
-func (n *ovn) Start() error {
-	if n.status == api.NetworkStatusPending {
-		return fmt.Errorf("Cannot start pending network")
-	}
-
-	return nil
-}
-
-// Stop stops is a no-op.
-func (n *ovn) Stop() error {
-	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 *ovn) Update(newNetwork api.NetworkPut, targetNode string, clusterNotification bool) error {
-	n.logger.Debug("Update", log.Ctx{"clusterNotification": clusterNotification, "newNetwork": newNetwork})
-
-	// Populate default values if they are missing.
-	err := n.fillConfig(newNetwork.Config)
-	if err != nil {
-		return err
-	}
-
-	// Populate auto fields.
-	err = fillAuto(newNetwork.Config)
-	if err != nil {
-		return err
-	}
-
-	dbUpdateNeeeded, changedKeys, 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, clusterNotification)
-
-		// Reset any change that was made to network.
-		if !clusterNotification {
-			n.setup(true)
-		}
-	})
-
-	// Apply changes to database.
-	err = n.common.update(newNetwork, targetNode, clusterNotification)
-	if err != nil {
-		return err
-	}
-
-	// Restart the network if needed.
-	if len(changedKeys) > 0 && !clusterNotification {
-		err = n.setup(true)
-		if err != nil {
-			return err
-		}
-	}
-
-	revert.Success()
-	return nil
-}
-
-// getInstanceDevicePortName returns the switch port name to use for an instance device.
-func (n *ovn) getInstanceDevicePortName(v *ovnVars, instanceID int, deviceName string) openvswitch.OVNSwitchPort {
-	return openvswitch.OVNSwitchPort(fmt.Sprintf("%s-%d-%s", v.intSwitchInstancePortPrefix, instanceID, deviceName))
-}
-
-// instanceDevicePortAdd adds an instance device port to the internal logical switch and returns the port name.
-func (n *ovn) instanceDevicePortAdd(instanceID int, deviceName string, mac net.HardwareAddr, ips []net.IP) (openvswitch.OVNSwitchPort, error) {
-	var dhcpV4ID, dhcpv6ID string
-
-	revert := revert.New()
-	defer revert.Fail()
-
-	ovn, err := n.getOVNVars()
-	if err != nil {
-		return "", err
-	}
-
-	// Get DHCP options IDs.
-	if ovn.routerIntPortIPv4Net != "" {
-		_, routerIntPortIPv4Net, err := net.ParseCIDR(ovn.routerIntPortIPv4Net)
-		if err != nil {
-			return "", err
-		}
-
-		dhcpV4ID, err = ovn.client.LogicalSwitchDHCPOptionsGetID(ovn.intSwitchName, routerIntPortIPv4Net)
-		if err != nil {
-			return "", err
-		}
-	}
-
-	if ovn.routerIntPortIPv6Net != "" {
-		_, routerIntPortIPv6Net, err := net.ParseCIDR(ovn.routerIntPortIPv6Net)
-		if err != nil {
-			return "", err
-		}
-
-		dhcpv6ID, err = ovn.client.LogicalSwitchDHCPOptionsGetID(ovn.intSwitchName, routerIntPortIPv6Net)
-		if err != nil {
-			return "", err
-		}
-	}
-
-	instancePortName := n.getInstanceDevicePortName(ovn, instanceID, deviceName)
-
-	// Add port with mayExist set to true, so that if instance port exists, we don't fail and continue below
-	// to configure the port as needed. This is required in case the OVN northbound database was unavailable
-	// when the instance NIC was stopped and was unable to remove the port on last stop, which would otherwise
-	// prevent future NIC starts.
-	err = ovn.client.LogicalSwitchPortAdd(ovn.intSwitchName, instancePortName, true)
-	if err != nil {
-		return "", err
-	}
-
-	revert.Add(func() { ovn.client.LogicalSwitchPortDelete(instancePortName) })
-
-	err = ovn.client.LogicalSwitchPortSet(instancePortName, &openvswitch.OVNSwitchPortOpts{
-		DHCPv4OptsID: dhcpV4ID,
-		DHCPv6OptsID: dhcpv6ID,
-		MAC:          mac,
-		IPs:          ips,
-	})
-	if err != nil {
-		return "", err
-	}
-
-	revert.Success()
-	return instancePortName, nil
-}
-
-// instanceDevicePortDelete deletes an instance device port from the internal logical switch.
-func (n *ovn) instanceDevicePortDelete(instanceID int, deviceName string) error {
-	ovn, err := n.getOVNVars()
-	if err != nil {
-		return err
-	}
-
-	instancePortName := n.getInstanceDevicePortName(ovn, instanceID, deviceName)
-
-	err = ovn.client.LogicalSwitchPortDelete(instancePortName)
-	if err != nil {
-		return err
-	}
-
-	return nil
-}

From 651cd93557f4ff4ea27df68c57e48e8a482b8165 Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Mon, 3 Aug 2020 13:51:24 +0100
Subject: [PATCH 30/33] shared/validate: Makes IsNetworkAddressV6 non-optional

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

diff --git a/shared/validate/validate.go b/shared/validate/validate.go
index 0856e89f0d..dba07eef8b 100644
--- a/shared/validate/validate.go
+++ b/shared/validate/validate.go
@@ -248,10 +248,6 @@ func IsNetworkV6(value string) error {
 
 // IsNetworkAddressV6 validates an IPv6 addresss string. If string is empty, returns valid.
 func IsNetworkAddressV6(value string) error {
-	if value == "" {
-		return nil
-	}
-
 	ip := net.ParseIP(value)
 	if ip == nil || ip.To4() != nil {
 		return fmt.Errorf("Not an IPv6 address: %s", value)

From 1b71737b29c61308fc5de3d4a757788997377a53 Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Mon, 3 Aug 2020 13:57:10 +0100
Subject: [PATCH 31/33] lxd: Wraps validate.IsNetworkAddressV6 in
 validate.Optional

Signed-off-by: Thomas Parrott <thomas.parrott at canonical.com>
---
 lxd/device/nic.go            | 4 ++--
 lxd/device/nic_ipvlan.go     | 6 +-----
 lxd/device/nic_routed.go     | 8 +-------
 lxd/network/driver_bridge.go | 2 +-
 4 files changed, 5 insertions(+), 15 deletions(-)

diff --git a/lxd/device/nic.go b/lxd/device/nic.go
index 5a15c3e4b1..a1756aa055 100644
--- a/lxd/device/nic.go
+++ b/lxd/device/nic.go
@@ -24,14 +24,14 @@ func nicValidationRules(requiredFields []string, optionalFields []string) map[st
 		"maas.subnet.ipv4":        validate.IsAny,
 		"maas.subnet.ipv6":        validate.IsAny,
 		"ipv4.address":            validate.Optional(validate.IsNetworkAddressV4),
-		"ipv6.address":            validate.IsNetworkAddressV6,
+		"ipv6.address":            validate.Optional(validate.IsNetworkAddressV6),
 		"ipv4.routes":             validate.IsNetworkV4List,
 		"ipv6.routes":             validate.IsNetworkV6List,
 		"boot.priority":           validate.Optional(validate.IsUint32),
 		"ipv4.gateway":            networkValidGateway,
 		"ipv6.gateway":            networkValidGateway,
 		"ipv4.host_address":       validate.Optional(validate.IsNetworkAddressV4),
-		"ipv6.host_address":       validate.IsNetworkAddressV6,
+		"ipv6.host_address":       validate.Optional(validate.IsNetworkAddressV6),
 		"ipv4.host_table":         validate.Optional(validate.IsUint32),
 		"ipv6.host_table":         validate.Optional(validate.IsUint32),
 	}
diff --git a/lxd/device/nic_ipvlan.go b/lxd/device/nic_ipvlan.go
index e21efcfed4..fc99bd4a3b 100644
--- a/lxd/device/nic_ipvlan.go
+++ b/lxd/device/nic_ipvlan.go
@@ -121,11 +121,7 @@ func (d *nicIPVLAN) validateConfig(instConf instance.ConfigReader) error {
 		}
 
 		rules["ipv6.gateway"] = func(value string) error {
-			if value == "" {
-				return nil
-			}
-
-			return validate.IsNetworkAddressV6(value)
+			return validate.Optional(validate.IsNetworkAddressV6)(value)
 		}
 	}
 
diff --git a/lxd/device/nic_routed.go b/lxd/device/nic_routed.go
index 9a7f963e4c..7ac1b151b6 100644
--- a/lxd/device/nic_routed.go
+++ b/lxd/device/nic_routed.go
@@ -60,13 +60,7 @@ func (d *nicRouted) validateConfig(instConf instance.ConfigReader) error {
 
 		return validate.IsNetworkAddressV4List(value)
 	}
-	rules["ipv6.address"] = func(value string) error {
-		if value == "" {
-			return nil
-		}
-
-		return validate.IsNetworkAddressV6List(value)
-	}
+	rules["ipv6.address"] = validate.Optional(validate.IsNetworkAddressV6List)
 
 	err := d.config.Validate(rules)
 	if err != nil {
diff --git a/lxd/network/driver_bridge.go b/lxd/network/driver_bridge.go
index d96a13acfd..458819807c 100644
--- a/lxd/network/driver_bridge.go
+++ b/lxd/network/driver_bridge.go
@@ -202,7 +202,7 @@ func (n *bridge) Validate(config map[string]string) error {
 		"ipv6.nat.order": func(value string) error {
 			return validate.IsOneOf(value, []string{"before", "after"})
 		},
-		"ipv6.nat.address":   validate.IsNetworkAddressV6,
+		"ipv6.nat.address":   validate.Optional(validate.IsNetworkAddressV6),
 		"ipv6.dhcp":          validate.Optional(validate.IsBool),
 		"ipv6.dhcp.expiry":   validate.IsAny,
 		"ipv6.dhcp.stateful": validate.Optional(validate.IsBool),

From d177f8414b7700d5aa37e82bd20f85e0f3db505d Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Mon, 3 Aug 2020 14:02:15 +0100
Subject: [PATCH 32/33] lxd/device/nic/ipvlan: validate.IsNetworkAddressVX
 tweaks

Signed-off-by: Thomas Parrott <thomas.parrott at canonical.com>
---
 lxd/device/nic_ipvlan.go | 9 ++-------
 1 file changed, 2 insertions(+), 7 deletions(-)

diff --git a/lxd/device/nic_ipvlan.go b/lxd/device/nic_ipvlan.go
index fc99bd4a3b..86582085a1 100644
--- a/lxd/device/nic_ipvlan.go
+++ b/lxd/device/nic_ipvlan.go
@@ -116,13 +116,8 @@ func (d *nicIPVLAN) validateConfig(instConf instance.ConfigReader) error {
 	}
 
 	if d.config["mode"] == ipvlanModeL2 {
-		rules["ipv4.gateway"] = func(value string) error {
-			return validate.Optional(validate.IsNetworkAddressV4)(value)
-		}
-
-		rules["ipv6.gateway"] = func(value string) error {
-			return validate.Optional(validate.IsNetworkAddressV6)(value)
-		}
+		rules["ipv4.gateway"] = validate.Optional(validate.IsNetworkAddressV4)
+		rules["ipv6.gateway"] = validate.Optional(validate.IsNetworkAddressV6)
 	}
 
 	err := d.config.Validate(rules)

From a87bdd2a51c6a2bdc02181b0e5e159c273ae895b Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Mon, 3 Aug 2020 14:02:44 +0100
Subject: [PATCH 33/33] lxd/device/nic/routed: Wraps
 validate.IsNetworkAddressV4List in validate.Optional

Signed-off-by: Thomas Parrott <thomas.parrott at canonical.com>
---
 lxd/device/nic_routed.go | 8 +-------
 1 file changed, 1 insertion(+), 7 deletions(-)

diff --git a/lxd/device/nic_routed.go b/lxd/device/nic_routed.go
index 7ac1b151b6..460993e639 100644
--- a/lxd/device/nic_routed.go
+++ b/lxd/device/nic_routed.go
@@ -53,13 +53,7 @@ func (d *nicRouted) validateConfig(instConf instance.ConfigReader) error {
 	}
 
 	rules := nicValidationRules(requiredFields, optionalFields)
-	rules["ipv4.address"] = func(value string) error {
-		if value == "" {
-			return nil
-		}
-
-		return validate.IsNetworkAddressV4List(value)
-	}
+	rules["ipv4.address"] = validate.Optional(validate.IsNetworkAddressV4List)
 	rules["ipv6.address"] = validate.Optional(validate.IsNetworkAddressV6List)
 
 	err := d.config.Validate(rules)


More information about the lxc-devel mailing list