diff --git a/go.mod b/go.mod index 3d20f5e0..6b14fb69 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/Telmate/proxmox-api-go go 1.19 require ( + github.com/joho/godotenv v1.5.1 github.com/spf13/cobra v1.8.0 github.com/stretchr/testify v1.8.2 ) @@ -10,7 +11,6 @@ require ( require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/joho/godotenv v1.5.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/spf13/pflag v1.0.5 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/proxmox/config_guest.go b/proxmox/config_guest.go index 78746c14..3349e3da 100644 --- a/proxmox/config_guest.go +++ b/proxmox/config_guest.go @@ -2,11 +2,17 @@ package proxmox import ( "errors" + "net/netip" "strconv" ) // All code LXC and Qemu have in common should be placed here. +type GuestDNS struct { + NameServers *[]netip.Addr `json:"nameservers,omitempty"` + SearchDomain *string `json:"searchdomain,omitempty"` // we are not validating this field, as validating domain names is a complex topic. +} + type GuestResource struct { CpuCores uint `json:"cpu_cores"` CpuUsage float64 `json:"cpu_usage"` diff --git a/proxmox/config_qemu.go b/proxmox/config_qemu.go index 13180efd..95b29e13 100644 --- a/proxmox/config_qemu.go +++ b/proxmox/config_qemu.go @@ -8,7 +8,6 @@ import ( "log" "math/rand" "net" - "net/url" "regexp" "strconv" "strings" @@ -35,11 +34,9 @@ type ConfigQemu struct { Args string `json:"args,omitempty"` Balloon int `json:"balloon,omitempty"` // TODO should probably be a bool Bios string `json:"bios,omitempty"` - Boot string `json:"boot,omitempty"` // TODO should be an array of custom enums - BootDisk string `json:"bootdisk,omitempty"` // TODO discuss deprecation? Only returned as it's deprecated in the proxmox api - CIcustom string `json:"cicustom,omitempty"` // TODO should be part of a cloud-init struct (cloud-init option) - CIpassword string `json:"cipassword,omitempty"` // TODO should be part of a cloud-init struct (cloud-init option) - CIuser string `json:"ciuser,omitempty"` // TODO should be part of a cloud-init struct (cloud-init option) + Boot string `json:"boot,omitempty"` // TODO should be an array of custom enums + BootDisk string `json:"bootdisk,omitempty"` // TODO discuss deprecation? Only returned as it's deprecated in the proxmox api + CloudInit *CloudInit `json:"cloudinit,omitempty"` Description string `json:"description,omitempty"` Disks *QemuStorages `json:"disks,omitempty"` EFIDisk QemuDevice `json:"efidisk,omitempty"` // TODO should be a struct @@ -47,15 +44,13 @@ type ConfigQemu struct { HaGroup string `json:"hagroup,omitempty"` HaState string `json:"hastate,omitempty"` // TODO should be custom type with enum Hookscript string `json:"hookscript,omitempty"` - Hotplug string `json:"hotplug,omitempty"` // TODO should be a struct - Ipconfig IpconfigMap `json:"ipconfig,omitempty"` // TODO should be part of a cloud-init struct (cloud-init option) - Iso *IsoFile `json:"iso,omitempty"` // Same as Disks.Ide.Disk_2.CdRom.Iso - LinkedVmId uint `json:"linked_id,omitempty"` // Only returned setting it has no effect - Machine string `json:"machine,omitempty"` // TODO should be custom type with enum - Memory int `json:"memory,omitempty"` // TODO should be uint - Name string `json:"name,omitempty"` // TODO should be custom type as there are character and length limitations - Nameserver string `json:"nameserver,omitempty"` // TODO should be part of a cloud-init struct (cloud-init option) - Node string `json:"node,omitempty"` // Only returned setting it has no effect, set node in the VmRef instead + Hotplug string `json:"hotplug,omitempty"` // TODO should be a struct + Iso *IsoFile `json:"iso,omitempty"` // Same as Disks.Ide.Disk_2.CdRom.Iso + LinkedVmId uint `json:"linked_id,omitempty"` // Only returned setting it has no effect + Machine string `json:"machine,omitempty"` // TODO should be custom type with enum + Memory int `json:"memory,omitempty"` // TODO should be uint + Name string `json:"name,omitempty"` // TODO should be custom type as there are character and length limitations + Node string `json:"node,omitempty"` // Only returned setting it has no effect, set node in the VmRef instead Onboot *bool `json:"onboot,omitempty"` Pool *PoolName `json:"pool,omitempty"` Protection *bool `json:"protection,omitempty"` @@ -69,18 +64,16 @@ type ConfigQemu struct { QemuOs string `json:"ostype,omitempty"` QemuPCIDevices QemuDevices `json:"hostpci,omitempty"` // TODO should be a struct QemuPxe bool `json:"pxe,omitempty"` - QemuSerials QemuDevices `json:"serial,omitempty"` // TODO should be a struct - QemuSockets int `json:"sockets,omitempty"` // TODO should be uint - QemuUnusedDisks QemuDevices `json:"unused,omitempty"` // TODO should be a struct - QemuUsbs QemuDevices `json:"usb,omitempty"` // TODO should be a struct - QemuVcpus int `json:"vcpus,omitempty"` // TODO should be uint - QemuVga QemuDevice `json:"vga,omitempty"` // TODO should be a struct - RNGDrive QemuDevice `json:"rng0,omitempty"` // TODO should be a struct - Scsihw string `json:"scsihw,omitempty"` // TODO should be custom type with enum - Searchdomain string `json:"searchdomain,omitempty"` // TODO should be part of a cloud-init struct (cloud-init option) - Smbios1 string `json:"smbios1,omitempty"` // TODO should be custom type with enum? - Sshkeys string `json:"sshkeys,omitempty"` // TODO should be an array of strings - Startup string `json:"startup,omitempty"` // TODO should be a struct? + QemuSerials QemuDevices `json:"serial,omitempty"` // TODO should be a struct + QemuSockets int `json:"sockets,omitempty"` // TODO should be uint + QemuUnusedDisks QemuDevices `json:"unused,omitempty"` // TODO should be a struct + QemuUsbs QemuDevices `json:"usb,omitempty"` // TODO should be a struct + QemuVcpus int `json:"vcpus,omitempty"` // TODO should be uint + QemuVga QemuDevice `json:"vga,omitempty"` // TODO should be a struct + RNGDrive QemuDevice `json:"rng0,omitempty"` // TODO should be a struct + Scsihw string `json:"scsihw,omitempty"` // TODO should be custom type with enum + Smbios1 string `json:"smbios1,omitempty"` // TODO should be custom type with enum? + Startup string `json:"startup,omitempty"` // TODO should be a struct? TPM *TpmState `json:"tpm,omitempty"` Tablet *bool `json:"tablet,omitempty"` Tags *[]Tag `json:"tags,omitempty"` @@ -119,9 +112,6 @@ func (config *ConfigQemu) defaults() { if config.Hotplug == "" { config.Hotplug = "network,disk,usb" } - if config.Ipconfig == nil { - config.Ipconfig = IpconfigMap{} - } if config.Protection == nil { config.Protection = util.Pointer(false) } @@ -193,15 +183,6 @@ func (config ConfigQemu) mapToApiValues(currentConfig ConfigQemu) (rebootRequire if config.Boot != "" { params["boot"] = config.Boot } - if config.CIcustom != "" { - params["cicustom"] = config.CIcustom - } - if config.CIpassword != "" { - params["cipassword"] = config.CIpassword - } - if config.CIuser != "" { - params["ciuser"] = config.CIuser - } if config.QemuCores != 0 { params["cores"] = config.QemuCores } @@ -229,9 +210,6 @@ func (config ConfigQemu) mapToApiValues(currentConfig ConfigQemu) (rebootRequire if config.Name != "" { params["name"] = config.Name } - if config.Nameserver != "" { - params["nameserver"] = config.Nameserver - } if config.QemuNuma != nil { params["numa"] = *config.QemuNuma } @@ -247,15 +225,9 @@ func (config ConfigQemu) mapToApiValues(currentConfig ConfigQemu) (rebootRequire if config.Scsihw != "" { params["scsihw"] = config.Scsihw } - if config.Searchdomain != "" { - params["searchdomain"] = config.Searchdomain - } if config.QemuSockets != 0 { params["sockets"] = config.QemuSockets } - if config.Sshkeys != "" { - params["sshkeys"] = sshKeyUrlEncode(config.Sshkeys) - } if config.Startup != "" { params["startup"] = config.Startup } @@ -307,6 +279,10 @@ func (config ConfigQemu) mapToApiValues(currentConfig ConfigQemu) (rebootRequire } } + if config.CloudInit != nil { + itemsToDelete += config.CloudInit.mapToAPI(currentConfig.CloudInit, params) + } + // Create EFI disk config.CreateQemuEfiParams(params) @@ -330,13 +306,8 @@ func (config ConfigQemu) mapToApiValues(currentConfig ConfigQemu) (rebootRequire config.CreateQemuPCIsParams(params) - err = config.CreateIpconfigParams(params) - if err != nil { - log.Printf("[ERROR] %q", err) - } - if itemsToDelete != "" { - params["delete"] = itemsToDelete + params["delete"] = strings.TrimPrefix(itemsToDelete, ",") } return } @@ -351,7 +322,9 @@ func (ConfigQemu) mapToStruct(vmr *VmRef, params map[string]interface{}) (*Confi // description:Base image // cores:2 ostype:l26 - config := ConfigQemu{} + config := ConfigQemu{ + CloudInit: CloudInit{}.mapToSDK(params), + } if vmr != nil { config.Node = vmr.node @@ -382,15 +355,6 @@ func (ConfigQemu) mapToStruct(vmr *VmRef, params map[string]interface{}) (*Confi if _, isSet := params["bios"]; isSet { config.Bios = params["bios"].(string) } - if _, isSet := params["cicustom"]; isSet { - config.CIcustom = params["cicustom"].(string) - } - if _, isSet := params["cipassword"]; isSet { - config.CIpassword = params["cipassword"].(string) - } - if _, isSet := params["ciuser"]; isSet { - config.CIuser = params["ciuser"].(string) - } if _, isSet := params["description"]; isSet { config.Description = strings.TrimSpace(params["description"].(string)) } @@ -419,9 +383,6 @@ func (ConfigQemu) mapToStruct(vmr *VmRef, params map[string]interface{}) (*Confi if _, isSet := params["name"]; isSet { config.Name = params["name"].(string) } - if _, isSet := params["nameserver"]; isSet { - config.Nameserver = params["nameserver"].(string) - } if _, isSet := params["onboot"]; isSet { config.Onboot = util.Pointer(Itob(int(params["onboot"].(float64)))) } @@ -458,12 +419,6 @@ func (ConfigQemu) mapToStruct(vmr *VmRef, params map[string]interface{}) (*Confi if _, isSet := params["scsihw"]; isSet { config.Scsihw = params["scsihw"].(string) } - if _, isSet := params["searchdomain"]; isSet { - config.Searchdomain = params["searchdomain"].(string) - } - if _, isSet := params["sshkeys"]; isSet { - config.Sshkeys, _ = url.PathUnescape(params["sshkeys"].(string)) - } if _, isSet := params["startup"]; isSet { config.Startup = params["startup"].(string) } @@ -478,24 +433,6 @@ func (ConfigQemu) mapToStruct(vmr *VmRef, params map[string]interface{}) (*Confi config.Smbios1 = params["smbios1"].(string) } - ipconfigNames := []string{} - - for k := range params { - if ipconfigName := rxIpconfigName.FindStringSubmatch(k); len(ipconfigName) > 0 { - ipconfigNames = append(ipconfigNames, ipconfigName[0]) - } - } - - if len(ipconfigNames) > 0 { - config.Ipconfig = IpconfigMap{} - for _, ipconfigName := range ipconfigNames { - ipConfStr := params[ipconfigName] - id := rxDeviceID.FindStringSubmatch(ipconfigName) - ipconfigID, _ := strconv.Atoi(id[0]) - config.Ipconfig[ipconfigID] = ipConfStr - } - } - linkedVmId := uint(0) config.Disks = QemuStorages{}.mapToStruct(params, &linkedVmId) if linkedVmId != 0 { @@ -897,6 +834,11 @@ func (config ConfigQemu) Validate(current *ConfigQemu) (err error) { return } } + if config.CloudInit != nil { + if err = config.CloudInit.Validate(); err != nil { + return + } + } if config.Disks != nil { err = config.Disks.Validate() if err != nil { @@ -928,21 +870,6 @@ func (config ConfigQemu) Validate(current *ConfigQemu) (err error) { return } -// HasCloudInit - are there cloud-init options? -func (config ConfigQemu) HasCloudInit() bool { - for _, config := range config.Ipconfig { - if config != nil && config != "" { - return true - } - } - return config.CIuser != "" || - config.CIpassword != "" || - config.Searchdomain != "" || - config.Nameserver != "" || - config.Sshkeys != "" || - config.CIcustom != "" -} - /* CloneVm Example: Request @@ -1000,7 +927,6 @@ var ( rxSerialName = regexp.MustCompile(`serial\d+`) rxUsbName = regexp.MustCompile(`usb\d+`) rxPCIName = regexp.MustCompile(`hostpci\d+`) - rxIpconfigName = regexp.MustCompile(`ipconfig\d+`) ) func NewConfigQemuFromApi(vmr *VmRef, client *Client) (config *ConfigQemu, err error) { @@ -1087,16 +1013,6 @@ func SshForwardUsernet(vmr *VmRef, client *Client) (sshPort string, err error) { return } -// URL encodes the ssh keys -func sshKeyUrlEncode(keys string) (encodedKeys string) { - encodedKeys = url.PathEscape(keys + "\n") - encodedKeys = strings.Replace(encodedKeys, "+", "%2B", -1) - encodedKeys = strings.Replace(encodedKeys, "@", "%40", -1) - encodedKeys = strings.Replace(encodedKeys, "=", "%3D", -1) - encodedKeys = strings.Replace(encodedKeys, ":", "%3A", -1) - return -} - // device_del net1 // netdev_del net1 func RemoveSshForwardUsernet(vmr *VmRef, client *Client) (err error) { @@ -1328,23 +1244,6 @@ func (c ConfigQemu) CreateQemuNetworksParams(params map[string]interface{}) { } } -// Create parameters for each Cloud-Init ipconfig entry. -func (c ConfigQemu) CreateIpconfigParams(params map[string]interface{}) error { - - for ID, config := range c.Ipconfig { - if ID > 15 { - return fmt.Errorf("only up to 16 Cloud-Init network configurations supported (ipconfig[0-15]), skipping ipconfig%d", ID) - } - - if config != "" { - ipconfigName := "ipconfig" + strconv.Itoa(ID) - params[ipconfigName] = config - } - } - - return nil -} - // Create RNG parameter. func (c ConfigQemu) CreateQemuRngParams(params map[string]interface{}) { rngParam := QemuDeviceParam{} @@ -1501,3 +1400,49 @@ func (c ConfigQemu) String() string { jsConf, _ := json.Marshal(c) return string(jsConf) } + +type QemuNetworkInterfaceID uint8 + +const ( + QemuNetworkInterfaceID_Error_Invalid string = "network interface ID must be in the range 0-31" + + QemuNetworkInterfaceID0 QemuNetworkInterfaceID = 0 + QemuNetworkInterfaceID1 QemuNetworkInterfaceID = 1 + QemuNetworkInterfaceID2 QemuNetworkInterfaceID = 2 + QemuNetworkInterfaceID3 QemuNetworkInterfaceID = 3 + QemuNetworkInterfaceID4 QemuNetworkInterfaceID = 4 + QemuNetworkInterfaceID5 QemuNetworkInterfaceID = 5 + QemuNetworkInterfaceID6 QemuNetworkInterfaceID = 6 + QemuNetworkInterfaceID7 QemuNetworkInterfaceID = 7 + QemuNetworkInterfaceID8 QemuNetworkInterfaceID = 8 + QemuNetworkInterfaceID9 QemuNetworkInterfaceID = 9 + QemuNetworkInterfaceID10 QemuNetworkInterfaceID = 10 + QemuNetworkInterfaceID11 QemuNetworkInterfaceID = 11 + QemuNetworkInterfaceID12 QemuNetworkInterfaceID = 12 + QemuNetworkInterfaceID13 QemuNetworkInterfaceID = 13 + QemuNetworkInterfaceID14 QemuNetworkInterfaceID = 14 + QemuNetworkInterfaceID15 QemuNetworkInterfaceID = 15 + QemuNetworkInterfaceID16 QemuNetworkInterfaceID = 16 + QemuNetworkInterfaceID17 QemuNetworkInterfaceID = 17 + QemuNetworkInterfaceID18 QemuNetworkInterfaceID = 18 + QemuNetworkInterfaceID19 QemuNetworkInterfaceID = 19 + QemuNetworkInterfaceID20 QemuNetworkInterfaceID = 20 + QemuNetworkInterfaceID21 QemuNetworkInterfaceID = 21 + QemuNetworkInterfaceID22 QemuNetworkInterfaceID = 22 + QemuNetworkInterfaceID23 QemuNetworkInterfaceID = 23 + QemuNetworkInterfaceID24 QemuNetworkInterfaceID = 24 + QemuNetworkInterfaceID25 QemuNetworkInterfaceID = 25 + QemuNetworkInterfaceID26 QemuNetworkInterfaceID = 26 + QemuNetworkInterfaceID27 QemuNetworkInterfaceID = 27 + QemuNetworkInterfaceID28 QemuNetworkInterfaceID = 28 + QemuNetworkInterfaceID29 QemuNetworkInterfaceID = 29 + QemuNetworkInterfaceID30 QemuNetworkInterfaceID = 30 + QemuNetworkInterfaceID31 QemuNetworkInterfaceID = 31 +) + +func (id QemuNetworkInterfaceID) Validate() error { + if id > 31 { + return fmt.Errorf(QemuNetworkInterfaceID_Error_Invalid) + } + return nil +} diff --git a/proxmox/config_qemu_cloudinit.go b/proxmox/config_qemu_cloudinit.go new file mode 100644 index 00000000..db2f3c1e --- /dev/null +++ b/proxmox/config_qemu_cloudinit.go @@ -0,0 +1,733 @@ +package proxmox + +import ( + "crypto" + "errors" + "net" + "net/netip" + "net/url" + "regexp" + "strconv" + "strings" +) + +var regexMultipleNewlineEncoded = regexp.MustCompile(`(%0A)+`) +var regexMultipleSpaces = regexp.MustCompile(`\s+`) +var regexMultipleSpacesEncoded = regexp.MustCompile(`(%20)+`) + +// URL encodes the ssh keys +func sshKeyUrlDecode(encodedKeys string) (keys []crypto.PublicKey) { + encodedKeys = regexMultipleSpacesEncoded.ReplaceAllString(encodedKeys, "%20") + encodedKeys = strings.TrimSuffix(encodedKeys, "%0A") + encodedKeys = regexMultipleNewlineEncoded.ReplaceAllString(encodedKeys, "%0A") + encodedKeys = strings.ReplaceAll(encodedKeys, "%2B", "+") + encodedKeys = strings.ReplaceAll(encodedKeys, "%40", "@") + encodedKeys = strings.ReplaceAll(encodedKeys, "%3D", "=") + encodedKeys = strings.ReplaceAll(encodedKeys, "%3A", ":") + encodedKeys = strings.ReplaceAll(encodedKeys, "%20", " ") + encodedKeys = strings.ReplaceAll(encodedKeys, "%2F", "/") + for _, key := range strings.Split(encodedKeys, "%0A") { + keys = append(keys, key) + } + return +} + +// URL encodes the ssh keys +func sshKeyUrlEncode(keys []crypto.PublicKey) (encodedKeys string) { + for _, key := range keys { + tmpKey := regexMultipleSpaces.ReplaceAllString(key.(string), " ") + tmpKey = url.PathEscape(tmpKey + "\n") + tmpKey = strings.ReplaceAll(tmpKey, "+", "%2B") + tmpKey = strings.ReplaceAll(tmpKey, "@", "%40") + tmpKey = strings.ReplaceAll(tmpKey, "=", "%3D") + encodedKeys += strings.ReplaceAll(tmpKey, ":", "%3A") + } + return +} + +type CloudInit struct { + Custom *CloudInitCustom `json:"cicustom,omitempty"` + DNS *GuestDNS `json:"dns,omitempty"` + NetworkInterfaces CloudInitNetworkInterfaces `json:"ipconfig,omitempty"` + PublicSSHkeys *[]crypto.PublicKey `json:"sshkeys,omitempty"` + UpgradePackages *bool `json:"ciupgrade,omitempty"` + UserPassword *string `json:"userpassword,omitempty"` // TODO custom type + Username *string `json:"username,omitempty"` // TODO custom type +} + +func (config CloudInit) mapToAPI(current *CloudInit, params map[string]interface{}) (delete string) { + if current != nil { // Update + if config.Custom != nil { + params["cicustom"] = config.Custom.mapToAPI(current.Custom) + } + if config.Username != nil { + tmp := *config.Username + if tmp != "" { + params["ciuser"] = *config.Username + } else { + delete += ",ciuser" + } + } + if config.DNS != nil { + if config.DNS.SearchDomain != nil { + if *config.DNS.SearchDomain != "" { + params["searchdomain"] = *config.DNS.SearchDomain + } else { + delete += ",searchdomain" + } + } + if config.DNS.NameServers != nil { + if len(*config.DNS.NameServers) > 0 { + var nameservers string + for _, ns := range *config.DNS.NameServers { + nameservers += " " + ns.String() + } + params["nameserver"] = nameservers[1:] + } else { + delete += ",nameserver" + } + } + } + delete += config.NetworkInterfaces.mapToAPI(current.NetworkInterfaces, params) + if config.PublicSSHkeys != nil { + if len(*config.PublicSSHkeys) > 0 { + params["sshkeys"] = sshKeyUrlEncode(*config.PublicSSHkeys) + } else { + delete += ",sshkeys" + } + } + } else { // Create + if config.Custom != nil { + params["cicustom"] = config.Custom.mapToAPI(nil) + } + if config.Username != nil && *config.Username != "" { + params["ciuser"] = *config.Username + } + if config.DNS != nil { + if config.DNS.SearchDomain != nil && *config.DNS.SearchDomain != "" { + params["searchdomain"] = *config.DNS.SearchDomain + } + if config.DNS.NameServers != nil && len(*config.DNS.NameServers) > 0 { + var nameservers string + for _, ns := range *config.DNS.NameServers { + nameservers += " " + ns.String() + } + params["nameserver"] = nameservers[1:] + } + } + config.NetworkInterfaces.mapToAPI(nil, params) + if config.PublicSSHkeys != nil && len(*config.PublicSSHkeys) > 0 { + params["sshkeys"] = sshKeyUrlEncode(*config.PublicSSHkeys) + } + } + // Shared + if config.UpgradePackages != nil { + params["ciupgrade"] = Btoi(*config.UpgradePackages) + } + if config.UserPassword != nil { + params["cipassword"] = *config.UserPassword + } + return +} + +func (CloudInit) mapToSDK(params map[string]interface{}) *CloudInit { + ci := CloudInit{} + var set bool + if v, isSet := params["cicustom"]; isSet { + ci.Custom = CloudInitCustom{}.mapToSDK(v.(string)) + set = true + } + if v, isSet := params["cipassword"]; isSet { + tmp := v.(string) + ci.UserPassword = &tmp + set = true + } + if v, isSet := params["ciupgrade"]; isSet { + tmp := Itob(int(v.(float64))) + ci.UpgradePackages = &tmp + set = true + } + if v, isSet := params["ciuser"]; isSet { + tmp := v.(string) + if tmp != "" && tmp != " " { + ci.Username = &tmp + set = true + } + } + if v, isSet := params["sshkeys"]; isSet { + tmp := sshKeyUrlDecode(v.(string)) + ci.PublicSSHkeys = &tmp + set = true + } + var dnsSet bool + var nameservers []netip.Addr + if v, isSet := params["nameserver"]; isSet { + tmp := strings.Split(v.(string), " ") + nameservers = make([]netip.Addr, len(tmp)) + for i, e := range tmp { + nameservers[i], _ = netip.ParseAddr(e) + } + dnsSet = true + } + var domain string + if v, isSet := params["searchdomain"]; isSet { + if len(v.(string)) > 1 { + domain = v.(string) + dnsSet = true + } + } + if dnsSet { + ci.DNS = &GuestDNS{ + SearchDomain: &domain, + NameServers: &nameservers, + } + set = true + } + ci.NetworkInterfaces = CloudInitNetworkInterfaces{}.mapToSDK(params) + if set || len(ci.NetworkInterfaces) > 0 { + return &ci + } + return nil +} + +func (ci CloudInit) Validate() error { + if ci.Custom != nil { + if err := ci.Custom.Validate(); err != nil { + return err + } + } + return ci.NetworkInterfaces.Validate() +} + +type CloudInitCustom struct { + Meta *CloudInitSnippet `json:"meta,omitempty"` + Network *CloudInitSnippet `json:"network,omitempty"` + User *CloudInitSnippet `json:"user,omitempty"` + Vendor *CloudInitSnippet `json:"vendor,omitempty"` +} + +func (config CloudInitCustom) mapToAPI(current *CloudInitCustom) string { + var param string + if current != nil { // update + if config.Meta != nil { + param += config.Meta.mapToAPI("meta") + } else { + param += current.Meta.mapToAPI("meta") + } + if config.Network != nil { + param += config.Network.mapToAPI("network") + } else { + param += current.Network.mapToAPI("network") + } + if config.User != nil { + param += config.User.mapToAPI("user") + } else { + param += current.User.mapToAPI("user") + } + if config.Vendor != nil { + param += config.Vendor.mapToAPI("vendor") + } else { + param += current.Vendor.mapToAPI("vendor") + } + } else { // create + if config.Meta != nil { + param += config.Meta.mapToAPI("meta") + } + if config.Network != nil { + param += config.Network.mapToAPI("network") + } + if config.User != nil { + param += config.User.mapToAPI("user") + } + if config.Vendor != nil { + param += config.Vendor.mapToAPI("vendor") + } + } + if param != "" { + return param[1:] + } + return "" +} + +func (CloudInitCustom) mapToSDK(raw string) *CloudInitCustom { + var set bool + var config CloudInitCustom + params := splitStringOfSettings(raw) + if v, isSet := params["meta"]; isSet { + config.Meta = CloudInitSnippet{}.mapToSDK(v.(string)) + set = true + } + if v, isSet := params["network"]; isSet { + config.Network = CloudInitSnippet{}.mapToSDK(v.(string)) + set = true + } + if v, isSet := params["user"]; isSet { + config.User = CloudInitSnippet{}.mapToSDK(v.(string)) + set = true + } + if v, isSet := params["vendor"]; isSet { + config.Vendor = CloudInitSnippet{}.mapToSDK(v.(string)) + set = true + } + if set { + return &config + } + return nil +} + +func (ci CloudInitCustom) Validate() (err error) { + if ci.Meta != nil { + if err = ci.Meta.Validate(); err != nil { + return + } + } + if ci.Network != nil { + if err = ci.Network.Validate(); err != nil { + return + } + } + if ci.User != nil { + if err = ci.User.Validate(); err != nil { + return err + } + } + if ci.Vendor != nil { + err = ci.Vendor.Validate() + } + return +} + +func (ci CloudInitCustom) String() string { + return ci.mapToAPI(nil) +} + +type CloudInitIPv4Config struct { + Address *IPv4CIDR `json:"address,omitempty"` + DHCP bool `json:"dhcp,omitempty"` + Gateway *IPv4Address `json:"gateway,omitempty"` +} + +const CloudInitIPv4Config_Error_DhcpAddressMutuallyExclusive string = "ipv4 dhcp is mutually exclusive with address" +const CloudInitIPv4Config_Error_DhcpGatewayMutuallyExclusive string = "ipv4 dhcp is mutually exclusive with gateway" + +func (config CloudInitIPv4Config) mapToAPI(current *CloudInitIPv4Config) string { + // config can only be nil during update + if config.DHCP { + return ",ip=dhcp" + } + if current != nil { // Update phase, Update value + var param string + if config.Address != nil { + if *config.Address != "" { + param = ",ip=" + string(*config.Address) + } + } else if current.Address != nil { + param = ",ip=" + string(*current.Address) + } + if config.Gateway != nil { + if *config.Gateway != "" { + param += ",gw=" + string(*config.Gateway) + } + } else if current.Gateway != nil { + param += ",gw=" + string(*current.Gateway) + } + return param + } + // Create phase + var param string + if config.Address != nil && *config.Address != "" { + param = ",ip=" + string(*config.Address) + } + if config.Gateway != nil && *config.Gateway != "" { + param += ",gw=" + string(*config.Gateway) + } + return param +} + +func (config CloudInitIPv4Config) String() string { + param := config.mapToAPI(nil) + if param != "" { + return param[1:] + } + return "" +} + +func (config CloudInitIPv4Config) Validate() error { + if config.Address != nil && *config.Address != "" { + if config.DHCP { + return errors.New(CloudInitIPv4Config_Error_DhcpAddressMutuallyExclusive) + } + if err := config.Address.Validate(); err != nil { + return err + } + } + if config.Gateway != nil && *config.Gateway != "" { + if config.DHCP { + return errors.New(CloudInitIPv4Config_Error_DhcpGatewayMutuallyExclusive) + } + if err := config.Gateway.Validate(); err != nil { + return err + } + } + return nil +} + +type CloudInitIPv6Config struct { + Address *IPv6CIDR `json:"address,omitempty"` + DHCP bool `json:"dhcp,omitempty"` + Gateway *IPv6Address `json:"gateway,omitempty"` + SLAAC bool `json:"slaac,omitempty"` +} + +func (config CloudInitIPv6Config) mapToAPI(current *CloudInitIPv6Config) string { + if config.DHCP { + return ",ip6=dhcp" + } + if config.SLAAC { + return ",ip6=auto" + } + if current != nil { // Update + var param string + if config.Address != nil { + if *config.Address != "" { + param = ",ip6=" + string(*config.Address) + } + } else if current.Address != nil { + param = ",ip6=" + string(*current.Address) + } + if config.Gateway != nil { + if *config.Gateway != "" { + param += ",gw6=" + string(*config.Gateway) + } + } else if current.Gateway != nil { + param += ",gw6=" + string(*current.Gateway) + } + return param + } + // create + var param string + if config.Address != nil && *config.Address != "" { + param = ",ip6=" + string(*config.Address) + } + if config.Gateway != nil && *config.Gateway != "" { + param += ",gw6=" + string(*config.Gateway) + } + return param +} + +func (config CloudInitIPv6Config) String() string { + param := config.mapToAPI(nil) + if param != "" { + return param[1:] + } + return "" +} + +const CloudInitIPv6Config_Error_DhcpAddressMutuallyExclusive string = "ipv6 dhcp is mutually exclusive with address" +const CloudInitIPv6Config_Error_DhcpGatewayMutuallyExclusive string = "ipv6 dhcp is mutually exclusive with gateway" +const CloudInitIPv6Config_Error_DhcpSlaacMutuallyExclusive string = "ipv6 dhcp is mutually exclusive with slaac" +const CloudInitIPv6Config_Error_SlaacAddressMutuallyExclusive string = "ipv6 slaac is mutually exclusive with address" +const CloudInitIPv6Config_Error_SlaacGatewayMutuallyExclusive string = "ipv6 slaac is mutually exclusive with gateway" + +func (config CloudInitIPv6Config) Validate() error { + if config.DHCP && config.SLAAC { + return errors.New(CloudInitIPv6Config_Error_DhcpSlaacMutuallyExclusive) + } + if config.Address != nil && *config.Address != "" { + if config.DHCP { + return errors.New(CloudInitIPv6Config_Error_DhcpAddressMutuallyExclusive) + } + if config.SLAAC { + return errors.New(CloudInitIPv6Config_Error_SlaacAddressMutuallyExclusive) + } + if err := config.Address.Validate(); err != nil { + return err + } + } + if config.Gateway != nil && *config.Gateway != "" { + if config.DHCP { + return errors.New(CloudInitIPv6Config_Error_DhcpGatewayMutuallyExclusive) + } + if config.SLAAC { + return errors.New(CloudInitIPv6Config_Error_SlaacGatewayMutuallyExclusive) + } + if err := config.Gateway.Validate(); err != nil { + return err + } + } + return nil +} + +type CloudInitNetworkConfig struct { + IPv4 *CloudInitIPv4Config `json:"ip4,omitempty"` + IPv6 *CloudInitIPv6Config `json:"ip6,omitempty"` +} + +func (config CloudInitNetworkConfig) mapToAPI(current *CloudInitNetworkConfig) (param string) { + if current != nil { // Update + if config.IPv4 != nil { + param += config.IPv4.mapToAPI(current.IPv4) + } else { + if current.IPv4 != nil { + param += current.IPv4.mapToAPI(nil) + } + } + if config.IPv6 != nil { + param += config.IPv6.mapToAPI(current.IPv6) + } else { + if current.IPv6 != nil { + param += current.IPv6.mapToAPI(nil) + } + } + } else { // Create + if config.IPv4 != nil { + param += config.IPv4.mapToAPI(nil) + } + if config.IPv6 != nil { + param += config.IPv6.mapToAPI(nil) + } + } + return +} + +func (CloudInitNetworkConfig) mapToSDK(param string) (config CloudInitNetworkConfig) { + params := splitStringOfSettings(param) + var ipv4Set bool + var ipv6Set bool + var ipv4 CloudInitIPv4Config + var ipv6 CloudInitIPv6Config + if v, isSet := params["ip"]; isSet { + ipv4Set = true + if v.(string) == "dhcp" { + ipv4.DHCP = true + } else { + tmp := IPv4CIDR(v.(string)) + ipv4.Address = &tmp + } + } + if v, isSet := params["gw"]; isSet { + ipv4Set = true + tmp := IPv4Address(v.(string)) + ipv4.Gateway = &tmp + } + if v, isSet := params["ip6"]; isSet { + ipv6Set = true + if v.(string) == "dhcp" { + ipv6.DHCP = true + } else if v.(string) == "auto" { + ipv6.SLAAC = true + } else { + tmp := IPv6CIDR(v.(string)) + ipv6.Address = &tmp + } + } + if v, isSet := params["gw6"]; isSet { + ipv6Set = true + tmp := IPv6Address(v.(string)) + ipv6.Gateway = &tmp + } + if ipv4Set { + config.IPv4 = &ipv4 + } + if ipv6Set { + config.IPv6 = &ipv6 + } + return +} + +func (config CloudInitNetworkConfig) Validate() (err error) { + if config.IPv4 != nil { + if err = config.IPv4.Validate(); err != nil { + return + } + } + if config.IPv6 != nil { + err = config.IPv6.Validate() + } + return +} + +type CloudInitNetworkInterfaces map[QemuNetworkInterfaceID]CloudInitNetworkConfig + +func (interfaces CloudInitNetworkInterfaces) mapToAPI(current CloudInitNetworkInterfaces, params map[string]interface{}) (delete string) { + for i, e := range interfaces { + var tmpCurrent *CloudInitNetworkConfig + if current != nil { + if _, isSet := current[i]; isSet { + tmp := current[i] + tmpCurrent = &tmp + } + } + param := e.mapToAPI(tmpCurrent) + if param != "" { + params["ipconfig"+strconv.FormatInt(int64(i), 10)] = param[1:] + } else if tmpCurrent != nil { + delete += ",ipconfig" + strconv.FormatInt(int64(i), 10) + } + } + return +} + +func (CloudInitNetworkInterfaces) mapToSDK(params map[string]interface{}) CloudInitNetworkInterfaces { + ci := make(CloudInitNetworkInterfaces) + for i := QemuNetworkInterfaceID(0); i < 32; i++ { + if v, isSet := params["ipconfig"+strconv.FormatInt(int64(i), 10)]; isSet { + tmp := v.(string) + if len(tmp) > 1 { // can be "" or " " + ci[i] = CloudInitNetworkConfig{}.mapToSDK(tmp) + } + } + } + return ci +} + +func (interfaces CloudInitNetworkInterfaces) Validate() (err error) { + for i := range interfaces { + if err = i.Validate(); err != nil { + return + } + if err = interfaces[i].Validate(); err != nil { + return + } + } + return +} + +// If either Storage or FilePath is empty, the snippet will be removed +type CloudInitSnippet struct { + FilePath CloudInitSnippetPath `json:"path,omitempty"` + Storage string `json:"storage,omitempty"` // TODO custom type (storage) +} + +func (ci CloudInitSnippet) mapToAPI(kind string) string { + tmp := ci.String() + if tmp != ":" { + return "," + kind + "=" + tmp + } + return "" +} + +func (CloudInitSnippet) mapToSDK(param string) *CloudInitSnippet { + file := strings.SplitN(param, ":", 2) + if len(file) == 2 { + return &CloudInitSnippet{ + Storage: file[0], + FilePath: CloudInitSnippetPath(file[1])} + } + return nil +} + +func (config CloudInitSnippet) String() string { + return config.Storage + ":" + string(config.FilePath) +} + +func (ci CloudInitSnippet) Validate() error { + if ci.FilePath != "" { + return ci.FilePath.Validate() + } + return nil +} + +type CloudInitSnippetPath string + +var ( + regexCloudInitSnippetPath_Charters = regexp.MustCompile(`^[a-zA-Z0-9- _\/.]+$`) + regexCloudInitSnippetPath_Path = regexp.MustCompile(`^[^,=/]+(\/[^,=/]+)*$`) +) + +const ( + CloudInitSnippetPath_Error_Empty = "cloudInitSnippetPath may not be empty" + CloudInitSnippetPath_Error_InvalidCharacters = "cloudInitSnippetPath may ony contain the following characters: [a-zA-Z0-9_ -/.]" + CloudInitSnippetPath_Error_InvalidPath = "cloudInitSnippetPath must be a valid unix path" + CloudInitSnippetPath_Error_MaxLength = "cloudInitSnippetPath may not be longer than 256 characters" + CloudInitSnippetPath_Error_Relative = "cloudInitSnippetPath must be an relative path" +) + +func (path CloudInitSnippetPath) Validate() error { + if path == "" { + return errors.New(CloudInitSnippetPath_Error_Empty) + } + if path[:1] == "/" { + return errors.New(CloudInitSnippetPath_Error_Relative) + } + if len(path) > 256 { + return errors.New(CloudInitSnippetPath_Error_MaxLength) + } + if !regexCloudInitSnippetPath_Charters.MatchString(string(path)) { + return errors.New(CloudInitSnippetPath_Error_InvalidCharacters) + } + if !regexCloudInitSnippetPath_Path.MatchString(string(path)) { + return errors.New(CloudInitSnippetPath_Error_InvalidPath) + } + return nil +} + +type IPv4Address string + +const IPv4Address_Error_Invalid = "ipv4Address is not a valid ipv6 address" + +func (ip IPv4Address) Validate() error { + if ip == "" { + return nil + } + if net.ParseIP(string(ip)) == nil { + return errors.New(IPv4Address_Error_Invalid) + } + if !isIPv4(string(ip)) { + return errors.New(IPv4Address_Error_Invalid) + } + return nil +} + +type IPv4CIDR string + +const IPv4CIDR_Error_Invalid = "ipv4CIDR is not a valid ipv4 address" + +func (cidr IPv4CIDR) Validate() error { + if cidr == "" { + return nil + } + ip, _, err := net.ParseCIDR(string(cidr)) + if err != nil { + return errors.New(IPv4CIDR_Error_Invalid) + } + if !isIPv4(ip.String()) { + return errors.New(IPv4CIDR_Error_Invalid) + } + return err +} + +type IPv6Address string + +const IPv6Address_Error_Invalid = "ipv6Address is not a valid ipv6 address" + +func (ip IPv6Address) Validate() error { + if ip == "" { + return nil + } + if net.ParseIP(string(ip)) == nil { + return errors.New(IPv6Address_Error_Invalid) + } + if !isIPv6(string(ip)) { + return errors.New(IPv6Address_Error_Invalid) + } + return nil +} + +type IPv6CIDR string + +const IPv6CIDR_Error_Invalid = "ipv6CIDR is not a valid ipv6 address" + +func (cidr IPv6CIDR) Validate() error { + if cidr == "" { + return nil + } + ip, _, err := net.ParseCIDR(string(cidr)) + if err != nil { + return errors.New(IPv6CIDR_Error_Invalid) + } + if !isIPv6(ip.String()) { + return errors.New(IPv6CIDR_Error_Invalid) + } + return nil +} diff --git a/proxmox/config_qemu_cloudinit_test.go b/proxmox/config_qemu_cloudinit_test.go new file mode 100644 index 00000000..526d3231 --- /dev/null +++ b/proxmox/config_qemu_cloudinit_test.go @@ -0,0 +1,639 @@ +package proxmox + +import ( + "crypto" + "errors" + "testing" + + "github.com/Telmate/proxmox-api-go/internal/util" + "github.com/Telmate/proxmox-api-go/test/data/test_data_qemu" + "github.com/stretchr/testify/require" +) + +func Test_sshKeyUrlDecode(t *testing.T) { + tests := []struct { + name string + input string + output []crypto.PublicKey + }{ + {name: "Decode", + input: test_data_qemu.PublicKey_Encoded_Input(), + output: test_data_qemu.PublicKey_Decoded_Output()}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require.Equal(t, test.output, sshKeyUrlDecode(test.input)) + }) + } +} + +// Test the encoding logic to encode the ssh keys +func Test_sshKeyUrlEncode(t *testing.T) { + tests := []struct { + name string + input []crypto.PublicKey + output string + }{ + {name: "Encode", + input: test_data_qemu.PublicKey_Decoded_Input(), + output: test_data_qemu.PublicKey_Encoded_Output()}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require.Equal(t, test.output, sshKeyUrlEncode(test.input)) + }) + } +} + +func Test_CloudInit_Validate(t *testing.T) { + tests := []struct { + name string + input CloudInit + output error + }{ + {name: `Valid CloudInit CloudInitCustom FilePath`, + input: CloudInit{Custom: &CloudInitCustom{ + Meta: &CloudInitSnippet{ + FilePath: CloudInitSnippetPath(test_data_qemu.CloudInitSnippetPath_Max_Legal())}}}}, + {name: `Valid CloudInit CloudInitCustom FilePath empty`, + input: CloudInit{Custom: &CloudInitCustom{Network: &CloudInitSnippet{}}}}, + {name: `Valid CloudInit CloudInitNetworkInterfaces IPv4 Address`, + input: CloudInit{NetworkInterfaces: CloudInitNetworkInterfaces{QemuNetworkInterfaceID0: CloudInitNetworkConfig{ + IPv4: util.Pointer(CloudInitIPv4Config{Address: util.Pointer(IPv4CIDR("192.168.45.1/24"))})}}}}, + {name: `Valid CloudInit CloudInitNetworkInterfaces IPv4 Address empty`, + input: CloudInit{NetworkInterfaces: CloudInitNetworkInterfaces{QemuNetworkInterfaceID1: CloudInitNetworkConfig{ + IPv4: util.Pointer(CloudInitIPv4Config{Address: util.Pointer(IPv4CIDR(""))})}}}}, + {name: `Valid CloudInit CloudInitNetworkInterfaces IPv4 DHCP Address empty`, + input: CloudInit{NetworkInterfaces: CloudInitNetworkInterfaces{QemuNetworkInterfaceID2: CloudInitNetworkConfig{ + IPv4: util.Pointer(CloudInitIPv4Config{ + Address: util.Pointer(IPv4CIDR("")), + DHCP: true})}}}}, + {name: `Valid CloudInit CloudInitNetworkInterfaces IPv4 DHCP Gateway empty`, + input: CloudInit{NetworkInterfaces: CloudInitNetworkInterfaces{QemuNetworkInterfaceID3: CloudInitNetworkConfig{ + IPv4: util.Pointer(CloudInitIPv4Config{ + Gateway: util.Pointer(IPv4Address("")), + DHCP: true})}}}}, + {name: `Valid CloudInit CloudInitNetworkInterfaces IPv4 Gateway`, + input: CloudInit{NetworkInterfaces: CloudInitNetworkInterfaces{QemuNetworkInterfaceID4: CloudInitNetworkConfig{ + IPv4: util.Pointer(CloudInitIPv4Config{Gateway: util.Pointer(IPv4Address("192.168.45.1"))})}}}}, + {name: `Valid CloudInit CloudInitNetworkInterfaces IPv4 Gateway empty`, + input: CloudInit{NetworkInterfaces: CloudInitNetworkInterfaces{QemuNetworkInterfaceID4: CloudInitNetworkConfig{ + IPv4: util.Pointer(CloudInitIPv4Config{Gateway: util.Pointer(IPv4Address(""))})}}}}, + {name: `Valid CloudInit CloudInitNetworkInterfaces IPv6 Address`, + input: CloudInit{NetworkInterfaces: CloudInitNetworkInterfaces{QemuNetworkInterfaceID9: CloudInitNetworkConfig{ + IPv6: util.Pointer(CloudInitIPv6Config{Address: util.Pointer(IPv6CIDR("2001:0db8:85a3::/64"))})}}}}, + {name: `Valid CloudInit CloudInitNetworkInterfaces IPv6 Address empty`, + input: CloudInit{NetworkInterfaces: CloudInitNetworkInterfaces{QemuNetworkInterfaceID10: CloudInitNetworkConfig{ + IPv6: util.Pointer(CloudInitIPv6Config{Address: util.Pointer(IPv6CIDR(""))})}}}}, + {name: `Valid CloudInit CloudInitNetworkInterfaces IPv6 DHCP Address empty`, + input: CloudInit{NetworkInterfaces: CloudInitNetworkInterfaces{QemuNetworkInterfaceID11: CloudInitNetworkConfig{ + IPv6: util.Pointer(CloudInitIPv6Config{ + Address: util.Pointer(IPv6CIDR("")), + DHCP: true})}}}}, + {name: `Valid CloudInit CloudInitNetworkInterfaces IPv6 DHCP Gateway empty`, + input: CloudInit{NetworkInterfaces: CloudInitNetworkInterfaces{QemuNetworkInterfaceID12: CloudInitNetworkConfig{ + IPv6: util.Pointer(CloudInitIPv6Config{ + Gateway: util.Pointer(IPv6Address("")), + DHCP: true})}}}}, + {name: `Valid CloudInit CloudInitNetworkInterfaces IPv6 Gateway`, + input: CloudInit{NetworkInterfaces: CloudInitNetworkInterfaces{QemuNetworkInterfaceID13: CloudInitNetworkConfig{ + IPv6: util.Pointer(CloudInitIPv6Config{Gateway: util.Pointer(IPv6Address("3f6d:5b2a:1e4d:7c91:abcd:1234:5678:9abc"))})}}}}, + {name: `Valid CloudInit CloudInitNetworkInterfaces IPv6 Gateway empty`, + input: CloudInit{NetworkInterfaces: CloudInitNetworkInterfaces{QemuNetworkInterfaceID14: CloudInitNetworkConfig{ + IPv6: util.Pointer(CloudInitIPv6Config{Gateway: util.Pointer(IPv6Address(""))})}}}}, + {name: `Valid CloudInit CloudInitNetworkInterfaces IPv6 SLAAC Address empty`, + input: CloudInit{NetworkInterfaces: CloudInitNetworkInterfaces{QemuNetworkInterfaceID15: CloudInitNetworkConfig{ + IPv6: util.Pointer(CloudInitIPv6Config{ + Address: util.Pointer(IPv6CIDR("")), + SLAAC: true})}}}}, + {name: `Valid CloudInit CloudInitNetworkInterfaces IPv6 SLAAC Gateway empty`, + input: CloudInit{NetworkInterfaces: CloudInitNetworkInterfaces{QemuNetworkInterfaceID16: CloudInitNetworkConfig{ + IPv6: util.Pointer(CloudInitIPv6Config{ + Gateway: util.Pointer(IPv6Address("")), + SLAAC: true})}}}}, + {name: `Invalid errors.New(CloudInitSnippetPath_Error_InvalidCharacters)`, + input: CloudInit{Custom: &CloudInitCustom{User: &CloudInitSnippet{ + FilePath: CloudInitSnippetPath(test_data_qemu.CloudInitSnippetPath_Character_Illegal()[0])}}}, + output: errors.New(CloudInitSnippetPath_Error_InvalidCharacters)}, + {name: `Invalid errors.New(CloudInitSnippetPath_Error_InvalidPath)`, + input: CloudInit{Custom: &CloudInitCustom{Vendor: &CloudInitSnippet{ + FilePath: CloudInitSnippetPath(test_data_qemu.CloudInitSnippetPath_InvalidPath())}}}, + output: errors.New(CloudInitSnippetPath_Error_InvalidPath)}, + {name: `Invalid errors.New(CloudInitSnippetPath_Error_MaxLength)`, + input: CloudInit{Custom: &CloudInitCustom{Meta: &CloudInitSnippet{ + FilePath: CloudInitSnippetPath(test_data_qemu.CloudInitSnippetPath_Max_Illegal())}}}, + output: errors.New(CloudInitSnippetPath_Error_MaxLength)}, + {name: `Invalid errors.New(CloudInitSnippetPath_Error_Relative)`, + input: CloudInit{Custom: &CloudInitCustom{Network: &CloudInitSnippet{ + FilePath: CloudInitSnippetPath(test_data_qemu.CloudInitSnippetPath_Relative())}}}, + output: errors.New(CloudInitSnippetPath_Error_Relative)}, + {name: `Invalid errors.New(QemuNetworkInterfaceID_Error_Invalid)`, + input: CloudInit{NetworkInterfaces: CloudInitNetworkInterfaces{32: CloudInitNetworkConfig{}}}, + output: errors.New(QemuNetworkInterfaceID_Error_Invalid)}, + {name: `Invalid CloudInit CloudInitNetworkInterfaces IPv4 Address Mutually exclusive with DHCP`, + input: CloudInit{NetworkInterfaces: CloudInitNetworkInterfaces{QemuNetworkInterfaceID5: CloudInitNetworkConfig{ + IPv4: util.Pointer(CloudInitIPv4Config{ + Address: util.Pointer(IPv4CIDR("192.168.45.1/24")), + DHCP: true})}}}, + output: errors.New(CloudInitIPv4Config_Error_DhcpAddressMutuallyExclusive)}, + {name: `Invalid CloudInit CloudInitNetworkInterfaces IPv4 Gateway Mutually exclusive with DHCP`, + input: CloudInit{NetworkInterfaces: CloudInitNetworkInterfaces{QemuNetworkInterfaceID6: CloudInitNetworkConfig{ + IPv4: util.Pointer(CloudInitIPv4Config{ + Gateway: util.Pointer(IPv4Address("192.168.45.1")), + DHCP: true})}}}, + output: errors.New(CloudInitIPv4Config_Error_DhcpGatewayMutuallyExclusive)}, + {name: `Invalid CloudInit CloudInitNetworkInterfaces IPv4 Address errors.New(IPv4CIDR_Error_Invalid)`, + input: CloudInit{NetworkInterfaces: CloudInitNetworkInterfaces{QemuNetworkInterfaceID7: CloudInitNetworkConfig{ + IPv4: util.Pointer(CloudInitIPv4Config{Address: util.Pointer(IPv4CIDR("192.168.45.1"))})}}}, + output: errors.New(IPv4CIDR_Error_Invalid)}, + {name: `Invalid CloudInit CloudInitNetworkInterfaces IPv4 Gateway errors.New(IPv4Address_Error_Invalid)`, + input: CloudInit{NetworkInterfaces: CloudInitNetworkInterfaces{QemuNetworkInterfaceID8: CloudInitNetworkConfig{ + IPv4: util.Pointer(CloudInitIPv4Config{Gateway: util.Pointer(IPv4Address("192.168.45.1/24"))})}}}, + output: errors.New(IPv4Address_Error_Invalid)}, + {name: `Invalid CloudInit CloudInitNetworkInterfaces IPv6 Address Mutually exclusive with DHCP`, + input: CloudInit{NetworkInterfaces: CloudInitNetworkInterfaces{QemuNetworkInterfaceID17: CloudInitNetworkConfig{ + IPv6: util.Pointer(CloudInitIPv6Config{ + Address: util.Pointer(IPv6CIDR("2001:0db8:85a3::/64")), + DHCP: true})}}}, + output: errors.New(CloudInitIPv6Config_Error_DhcpAddressMutuallyExclusive)}, + {name: `Invalid CloudInit CloudInitNetworkInterfaces IPv6 Address Mutually exclusive with SLAAC`, + input: CloudInit{NetworkInterfaces: CloudInitNetworkInterfaces{QemuNetworkInterfaceID18: CloudInitNetworkConfig{ + IPv6: util.Pointer(CloudInitIPv6Config{ + Address: util.Pointer(IPv6CIDR("2001:0db8:85a3::/64")), + SLAAC: true})}}}, + output: errors.New(CloudInitIPv6Config_Error_SlaacAddressMutuallyExclusive)}, + {name: `Invalid CloudInit CloudInitNetworkInterfaces IPv6 DHCP Mutually exclusive with SLAAC`, + input: CloudInit{NetworkInterfaces: CloudInitNetworkInterfaces{QemuNetworkInterfaceID19: CloudInitNetworkConfig{ + IPv6: util.Pointer(CloudInitIPv6Config{ + DHCP: true, + SLAAC: true})}}}, + output: errors.New(CloudInitIPv6Config_Error_DhcpSlaacMutuallyExclusive)}, + {name: `Invalid CloudInit CloudInitNetworkInterfaces IPv6 Gateway Mutually exclusive with DHCP`, + input: CloudInit{NetworkInterfaces: CloudInitNetworkInterfaces{QemuNetworkInterfaceID20: CloudInitNetworkConfig{ + IPv6: util.Pointer(CloudInitIPv6Config{ + Gateway: util.Pointer(IPv6Address("3f6d:5b2a:1e4d:7c91:abcd:1234:5678:9abc")), + DHCP: true})}}}, + output: errors.New(CloudInitIPv6Config_Error_DhcpGatewayMutuallyExclusive)}, + {name: `Invalid CloudInit CloudInitNetworkInterfaces IPv6 Gateway Mutually exclusive with SLAAC`, + input: CloudInit{NetworkInterfaces: CloudInitNetworkInterfaces{QemuNetworkInterfaceID21: CloudInitNetworkConfig{ + IPv6: util.Pointer(CloudInitIPv6Config{ + Gateway: util.Pointer(IPv6Address("3f6d:5b2a:1e4d:7c91:abcd:1234:5678:9abc")), + SLAAC: true})}}}, + output: errors.New(CloudInitIPv6Config_Error_SlaacGatewayMutuallyExclusive)}, + {name: `Invalid CloudInit CloudInitNetworkInterfaces IPv6 Address errors.New(IPv6CIDR_Error_Invalid)`, + input: CloudInit{NetworkInterfaces: CloudInitNetworkInterfaces{QemuNetworkInterfaceID22: CloudInitNetworkConfig{ + IPv6: util.Pointer(CloudInitIPv6Config{Address: util.Pointer(IPv6CIDR("3f6d:5b2a:1e4d:7c91:abcd:1234:5678:9abc"))})}}}, + output: errors.New(IPv6CIDR_Error_Invalid)}, + {name: `Invalid CloudInit CloudInitNetworkInterfaces IPv6 Gateway errors.New(IPv6Address_Error_Invalid)`, + input: CloudInit{NetworkInterfaces: CloudInitNetworkInterfaces{QemuNetworkInterfaceID23: CloudInitNetworkConfig{ + IPv6: util.Pointer(CloudInitIPv6Config{Gateway: util.Pointer(IPv6Address("2001:0db8:85a3::/64"))})}}}, + output: errors.New(IPv6Address_Error_Invalid)}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require.Equal(t, test.output, test.input.Validate()) + }) + } +} + +func Test_CloudInitCustom_Validate(t *testing.T) { + tests := []struct { + name string + input CloudInitCustom + output error + }{ + {name: `Valid CloudInitCustom FilePath`, + input: CloudInitCustom{Meta: &CloudInitSnippet{ + FilePath: CloudInitSnippetPath(test_data_qemu.CloudInitSnippetPath_Max_Legal())}}}, + {name: `Valid CloudInitCustom FilePath empty`, + input: CloudInitCustom{Network: &CloudInitSnippet{}}}, + {name: `Invalid errors.New(CloudInitSnippetPath_Error_InvalidCharacters`, + input: CloudInitCustom{User: &CloudInitSnippet{ + FilePath: CloudInitSnippetPath(test_data_qemu.CloudInitSnippetPath_Character_Illegal()[0])}}, + output: errors.New(CloudInitSnippetPath_Error_InvalidCharacters)}, + {name: `Invalid errors.New(CloudInitSnippetPath_Error_InvalidPath)`, + input: CloudInitCustom{Vendor: &CloudInitSnippet{ + FilePath: CloudInitSnippetPath(test_data_qemu.CloudInitSnippetPath_InvalidPath())}}, + output: errors.New(CloudInitSnippetPath_Error_InvalidPath)}, + {name: `Invalid errors.New(CloudInitSnippetPath_Error_MaxLength)`, + input: CloudInitCustom{Meta: &CloudInitSnippet{ + FilePath: CloudInitSnippetPath(test_data_qemu.CloudInitSnippetPath_Max_Illegal())}}, + output: errors.New(CloudInitSnippetPath_Error_MaxLength)}, + {name: `Invalid errors.New(CloudInitSnippetPath_Error_Relative)`, + input: CloudInitCustom{Network: &CloudInitSnippet{ + FilePath: CloudInitSnippetPath(test_data_qemu.CloudInitSnippetPath_Relative())}}, + output: errors.New(CloudInitSnippetPath_Error_Relative)}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require.Equal(t, test.output, test.input.Validate()) + }) + } +} + +func Test_CloudInitSnippet_Validate(t *testing.T) { + tests := []struct { + name string + input CloudInitSnippet + output error + }{ + {name: `Valid FilePath`, + input: CloudInitSnippet{FilePath: CloudInitSnippetPath(test_data_qemu.CloudInitSnippetPath_Max_Legal())}}, + {name: `Valid FilePath empty`}, + {name: `Invalid errors.New(CloudInitSnippetPath_Error_InvalidCharacters)`, + input: CloudInitSnippet{FilePath: CloudInitSnippetPath(test_data_qemu.CloudInitSnippetPath_Character_Illegal()[0])}, + output: errors.New(CloudInitSnippetPath_Error_InvalidCharacters)}, + {name: `Invalid errors.New(CloudInitSnippetPath_Error_InvalidPath)`, + input: CloudInitSnippet{FilePath: CloudInitSnippetPath(test_data_qemu.CloudInitSnippetPath_InvalidPath())}, + output: errors.New(CloudInitSnippetPath_Error_InvalidPath)}, + {name: `Invalid errors.New(CloudInitSnippetPath_Error_MaxLength)`, + input: CloudInitSnippet{FilePath: CloudInitSnippetPath(test_data_qemu.CloudInitSnippetPath_Max_Illegal())}, + output: errors.New(CloudInitSnippetPath_Error_MaxLength)}, + {name: `Invalid errors.New(CloudInitSnippetPath_Error_Relative)`, + input: CloudInitSnippet{FilePath: CloudInitSnippetPath(test_data_qemu.CloudInitSnippetPath_Relative())}, + output: errors.New(CloudInitSnippetPath_Error_Relative)}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require.Equal(t, test.output, test.input.Validate()) + }) + } +} + +func Test_CloudInitSnippetPath_Validate(t *testing.T) { + tests := []struct { + name string + input []string + output error + }{ + {name: `Valid`, + input: test_data_qemu.CloudInitSnippetPath_Legal()}, + {name: `Invalid errors.New(CloudInitSnippetPath_Error_Empty)`, + input: []string{test_data_qemu.CloudInitSnippetPath_Min_Illegal()}, + output: errors.New(CloudInitSnippetPath_Error_Empty)}, + {name: `Invalid errors.New(CloudInitSnippetPath_Error_InvalidCharacters)`, + input: test_data_qemu.CloudInitSnippetPath_Character_Illegal(), + output: errors.New(CloudInitSnippetPath_Error_InvalidCharacters)}, + {name: `Invalid errors.New(CloudInitSnippetPath_Error_InvalidPath)`, + input: []string{test_data_qemu.CloudInitSnippetPath_InvalidPath()}, + output: errors.New(CloudInitSnippetPath_Error_InvalidPath)}, + {name: `Invalid errors.New(CloudInitSnippetPath_Error_MaxLength)`, + input: []string{test_data_qemu.CloudInitSnippetPath_Max_Illegal()}, + output: errors.New(CloudInitSnippetPath_Error_MaxLength)}, + {name: `Invalid errors.New(CloudInitSnippetPath_Error_Relative)`, + input: []string{test_data_qemu.CloudInitSnippetPath_Relative()}, + output: errors.New(CloudInitSnippetPath_Error_Relative)}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + for _, input := range test.input { + require.Equal(t, test.output, CloudInitSnippetPath(input).Validate()) + } + }) + } +} + +func Test_CloudInitNetworkInterfaces_Validate(t *testing.T) { + tests := []struct { + name string + input CloudInitNetworkInterfaces + output error + }{ + {name: `Valid IPv4 Address`, + input: CloudInitNetworkInterfaces{QemuNetworkInterfaceID0: CloudInitNetworkConfig{ + IPv4: util.Pointer(CloudInitIPv4Config{Address: util.Pointer(IPv4CIDR("192.168.45.1/24"))})}}}, + {name: `Valid IPv4 Address empty`, + input: CloudInitNetworkInterfaces{QemuNetworkInterfaceID1: CloudInitNetworkConfig{ + IPv4: util.Pointer(CloudInitIPv4Config{Address: util.Pointer(IPv4CIDR(""))})}}}, + {name: `Valid IPv4 DHCP Address empty`, + input: CloudInitNetworkInterfaces{QemuNetworkInterfaceID2: CloudInitNetworkConfig{ + IPv4: util.Pointer(CloudInitIPv4Config{ + Address: util.Pointer(IPv4CIDR("")), + DHCP: true})}}}, + {name: `Valid IPv4 DHCP Gateway empty`, + input: CloudInitNetworkInterfaces{QemuNetworkInterfaceID3: CloudInitNetworkConfig{ + IPv4: util.Pointer(CloudInitIPv4Config{ + Gateway: util.Pointer(IPv4Address("")), + DHCP: true})}}}, + {name: `Valid IPv4 Gateway`, + input: CloudInitNetworkInterfaces{QemuNetworkInterfaceID4: CloudInitNetworkConfig{ + IPv4: util.Pointer(CloudInitIPv4Config{Gateway: util.Pointer(IPv4Address("192.168.45.1"))})}}}, + {name: `Valid IPv4 Gateway empty`, + input: CloudInitNetworkInterfaces{QemuNetworkInterfaceID5: CloudInitNetworkConfig{ + IPv4: util.Pointer(CloudInitIPv4Config{Gateway: util.Pointer(IPv4Address(""))})}}}, + {name: `Valid IPv6 Address`, + input: CloudInitNetworkInterfaces{QemuNetworkInterfaceID9: CloudInitNetworkConfig{ + IPv6: util.Pointer(CloudInitIPv6Config{Address: util.Pointer(IPv6CIDR("2001:0db8:85a3::/64"))})}}}, + {name: `Valid IPv6 Address empty`, + input: CloudInitNetworkInterfaces{QemuNetworkInterfaceID10: CloudInitNetworkConfig{ + IPv6: util.Pointer(CloudInitIPv6Config{Address: util.Pointer(IPv6CIDR(""))})}}}, + {name: `Valid IPv6 DHCP Address empty`, + input: CloudInitNetworkInterfaces{QemuNetworkInterfaceID11: CloudInitNetworkConfig{ + IPv6: util.Pointer(CloudInitIPv6Config{ + Address: util.Pointer(IPv6CIDR("")), + DHCP: true})}}}, + {name: `Valid IPv6 DHCP Gateway empty`, + input: CloudInitNetworkInterfaces{QemuNetworkInterfaceID12: CloudInitNetworkConfig{ + IPv6: util.Pointer(CloudInitIPv6Config{ + Gateway: util.Pointer(IPv6Address("")), + DHCP: true})}}}, + {name: `Valid IPv6 Gateway`, + input: CloudInitNetworkInterfaces{QemuNetworkInterfaceID13: CloudInitNetworkConfig{ + IPv6: util.Pointer(CloudInitIPv6Config{Gateway: util.Pointer(IPv6Address("3f6d:5b2a:1e4d:7c91:abcd:1234:5678:9abc"))})}}}, + {name: `Valid IPv6 Gateway empty`, + input: CloudInitNetworkInterfaces{QemuNetworkInterfaceID14: CloudInitNetworkConfig{ + IPv6: util.Pointer(CloudInitIPv6Config{Gateway: util.Pointer(IPv6Address(""))})}}}, + {name: `Valid IPv6 SLAAC Address empty`, + input: CloudInitNetworkInterfaces{QemuNetworkInterfaceID15: CloudInitNetworkConfig{ + IPv6: util.Pointer(CloudInitIPv6Config{ + Address: util.Pointer(IPv6CIDR("")), + SLAAC: true})}}}, + {name: `Valid IPv6 SLAAC Gateway empty`, + input: CloudInitNetworkInterfaces{QemuNetworkInterfaceID16: CloudInitNetworkConfig{ + IPv6: util.Pointer(CloudInitIPv6Config{ + Gateway: util.Pointer(IPv6Address("")), + SLAAC: true})}}}, + {name: `Invalid IPv4 Address Mutually exclusive with DHCP`, + input: CloudInitNetworkInterfaces{QemuNetworkInterfaceID5: CloudInitNetworkConfig{ + IPv4: util.Pointer(CloudInitIPv4Config{ + Address: util.Pointer(IPv4CIDR("192.168.45.1/24")), + DHCP: true})}}, + output: errors.New(CloudInitIPv4Config_Error_DhcpAddressMutuallyExclusive)}, + {name: `Invalid IPv4 Gateway Mutually exclusive with DHCP`, + input: CloudInitNetworkInterfaces{QemuNetworkInterfaceID6: CloudInitNetworkConfig{ + IPv4: util.Pointer(CloudInitIPv4Config{ + Gateway: util.Pointer(IPv4Address("192.168.45.1")), + DHCP: true})}}, + output: errors.New(CloudInitIPv4Config_Error_DhcpGatewayMutuallyExclusive)}, + {name: `Invalid IPv4 Address errors.New(IPv4CIDR_Error_Invalid)`, + input: CloudInitNetworkInterfaces{QemuNetworkInterfaceID7: CloudInitNetworkConfig{ + IPv4: util.Pointer(CloudInitIPv4Config{Address: util.Pointer(IPv4CIDR("192.168.45.1"))})}}, + output: errors.New(IPv4CIDR_Error_Invalid)}, + {name: `Invalid IPv4 Gateway errors.New(IPv4Address_Error_Invalid)`, + input: CloudInitNetworkInterfaces{QemuNetworkInterfaceID8: CloudInitNetworkConfig{ + IPv4: util.Pointer(CloudInitIPv4Config{Gateway: util.Pointer(IPv4Address("192.168.45.1/24"))})}}, + output: errors.New(IPv4Address_Error_Invalid)}, + {name: `Invalid IPv6 Address Mutually exclusive with DHCP`, + input: CloudInitNetworkInterfaces{QemuNetworkInterfaceID17: CloudInitNetworkConfig{ + IPv6: util.Pointer(CloudInitIPv6Config{ + Address: util.Pointer(IPv6CIDR("2001:0db8:85a3::/64")), + DHCP: true})}}, + output: errors.New(CloudInitIPv6Config_Error_DhcpAddressMutuallyExclusive)}, + {name: `Invalid IPv6 Address Mutually exclusive with SLAAC`, + input: CloudInitNetworkInterfaces{QemuNetworkInterfaceID18: CloudInitNetworkConfig{ + IPv6: util.Pointer(CloudInitIPv6Config{ + Address: util.Pointer(IPv6CIDR("2001:0db8:85a3::/64")), + SLAAC: true})}}, + output: errors.New(CloudInitIPv6Config_Error_SlaacAddressMutuallyExclusive)}, + {name: `Invalid IPv6 DHCP Mutually exclusive with SLAAC`, + input: CloudInitNetworkInterfaces{QemuNetworkInterfaceID19: CloudInitNetworkConfig{ + IPv6: util.Pointer(CloudInitIPv6Config{ + DHCP: true, + SLAAC: true})}}, + output: errors.New(CloudInitIPv6Config_Error_DhcpSlaacMutuallyExclusive)}, + {name: `Invalid IPv6 Gateway Mutually exclusive with DHCP`, + input: CloudInitNetworkInterfaces{QemuNetworkInterfaceID20: CloudInitNetworkConfig{ + IPv6: util.Pointer(CloudInitIPv6Config{ + Gateway: util.Pointer(IPv6Address("3f6d:5b2a:1e4d:7c91:abcd:1234:5678:9abc")), + DHCP: true})}}, + output: errors.New(CloudInitIPv6Config_Error_DhcpGatewayMutuallyExclusive)}, + {name: `Invalid IPv6 Gateway Mutually exclusive with SLAAC`, + input: CloudInitNetworkInterfaces{QemuNetworkInterfaceID21: CloudInitNetworkConfig{ + IPv6: util.Pointer(CloudInitIPv6Config{ + Gateway: util.Pointer(IPv6Address("3f6d:5b2a:1e4d:7c91:abcd:1234:5678:9abc")), + SLAAC: true})}}, + output: errors.New(CloudInitIPv6Config_Error_SlaacGatewayMutuallyExclusive)}, + {name: `Invalid IPv6 Address errors.New(IPv6CIDR_Error_Invalid)`, + input: CloudInitNetworkInterfaces{QemuNetworkInterfaceID22: CloudInitNetworkConfig{ + IPv6: util.Pointer(CloudInitIPv6Config{Address: util.Pointer(IPv6CIDR("3f6d:5b2a:1e4d:7c91:abcd:1234:5678:9abc"))})}}, + output: errors.New(IPv6CIDR_Error_Invalid)}, + {name: `Invalid IPv6 Gateway errors.New(IPv6Address_Error_Invalid)`, + input: CloudInitNetworkInterfaces{QemuNetworkInterfaceID23: CloudInitNetworkConfig{ + IPv6: util.Pointer(CloudInitIPv6Config{Gateway: util.Pointer(IPv6Address("2001:0db8:85a3::/64"))})}}, + output: errors.New(IPv6Address_Error_Invalid)}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require.Equal(t, test.output, test.input.Validate()) + }) + } +} + +func Test_CloudInitIPv4Config_Validate(t *testing.T) { + tests := []struct { + name string + input CloudInitIPv4Config + output error + }{ + {name: `Valid Address`, + input: CloudInitIPv4Config{Address: util.Pointer(IPv4CIDR("192.168.45.1/24"))}}, + {name: `Valid Address empty`, + input: CloudInitIPv4Config{Address: util.Pointer(IPv4CIDR(""))}}, + {name: `Valid DHCP Address empty`, + input: CloudInitIPv4Config{ + Address: util.Pointer(IPv4CIDR("")), + DHCP: true}}, + {name: `Valid DHCP Gateway empty`, + input: CloudInitIPv4Config{ + Gateway: util.Pointer(IPv4Address("")), + DHCP: true}}, + {name: `Valid Gateway`, + input: CloudInitIPv4Config{Gateway: util.Pointer(IPv4Address("192.168.45.1"))}}, + {name: `Valid Gateway empty`, + input: CloudInitIPv4Config{Gateway: util.Pointer(IPv4Address(""))}}, + {name: `Invalid Address Mutually exclusive with DHCP`, + input: CloudInitIPv4Config{ + Address: util.Pointer(IPv4CIDR("192.168.45.1/24")), + DHCP: true}, + output: errors.New(CloudInitIPv4Config_Error_DhcpAddressMutuallyExclusive)}, + {name: `Invalid Gateway Mutually exclusive with DHCP`, + input: CloudInitIPv4Config{ + Gateway: util.Pointer(IPv4Address("192.168.45.1")), + DHCP: true}, + output: errors.New(CloudInitIPv4Config_Error_DhcpGatewayMutuallyExclusive)}, + {name: `Invalid Address errors.New(IPv4CIDR_Error_Invalid)`, + input: CloudInitIPv4Config{Address: util.Pointer(IPv4CIDR("192.168.45.1"))}, + output: errors.New(IPv4CIDR_Error_Invalid)}, + {name: `Invalid Gateway errors.New(IPv4Address_Error_Invalid)`, + input: CloudInitIPv4Config{Gateway: util.Pointer(IPv4Address("192.168.45.1/24"))}, + output: errors.New(IPv4Address_Error_Invalid)}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require.Equal(t, test.output, test.input.Validate()) + }) + } +} + +func Test_CloudInitIPv6Config_Validate(t *testing.T) { + tests := []struct { + name string + input CloudInitIPv6Config + output error + }{ + {name: `Valid Address`, + input: CloudInitIPv6Config{Address: util.Pointer(IPv6CIDR("2001:0db8:85a3::/64"))}}, + {name: `Valid Address empty`, + input: CloudInitIPv6Config{Address: util.Pointer(IPv6CIDR(""))}}, + {name: `Valid DHCP Address empty`, + input: CloudInitIPv6Config{ + Address: util.Pointer(IPv6CIDR("")), + DHCP: true}}, + {name: `Valid DHCP Gateway empty`, + input: CloudInitIPv6Config{ + Gateway: util.Pointer(IPv6Address("")), + DHCP: true}}, + {name: `Valid Gateway`, + input: CloudInitIPv6Config{Gateway: util.Pointer(IPv6Address("3f6d:5b2a:1e4d:7c91:abcd:1234:5678:9abc"))}}, + {name: `Valid Gateway empty`, + input: CloudInitIPv6Config{Gateway: util.Pointer(IPv6Address(""))}}, + {name: `Valid SLAAC Address empty`, + input: CloudInitIPv6Config{ + Address: util.Pointer(IPv6CIDR("")), + SLAAC: true}}, + {name: `Valid SLAAC Gateway empty`, + input: CloudInitIPv6Config{ + Gateway: util.Pointer(IPv6Address("")), + SLAAC: true}}, + {name: `Invalid Address Mutually exclusive with DHCP`, + input: CloudInitIPv6Config{ + Address: util.Pointer(IPv6CIDR("2001:0db8:85a3::/64")), + DHCP: true}, + output: errors.New(CloudInitIPv6Config_Error_DhcpAddressMutuallyExclusive)}, + {name: `Invalid Address Mutually exclusive with SLAAC`, + input: CloudInitIPv6Config{ + Address: util.Pointer(IPv6CIDR("2001:0db8:85a3::/64")), + SLAAC: true}, + output: errors.New(CloudInitIPv6Config_Error_SlaacAddressMutuallyExclusive)}, + {name: `Invalid DHCP Mutually exclusive with SLAAC`, + input: CloudInitIPv6Config{ + DHCP: true, + SLAAC: true}, + output: errors.New(CloudInitIPv6Config_Error_DhcpSlaacMutuallyExclusive)}, + {name: `Invalid Gateway Mutually exclusive with DHCP`, + input: CloudInitIPv6Config{ + Gateway: util.Pointer(IPv6Address("3f6d:5b2a:1e4d:7c91:abcd:1234:5678:9abc")), + DHCP: true}, + output: errors.New(CloudInitIPv6Config_Error_DhcpGatewayMutuallyExclusive)}, + {name: `Invalid Gateway Mutually exclusive with SLAAC`, + input: CloudInitIPv6Config{ + Gateway: util.Pointer(IPv6Address("3f6d:5b2a:1e4d:7c91:abcd:1234:5678:9abc")), + SLAAC: true}, + output: errors.New(CloudInitIPv6Config_Error_SlaacGatewayMutuallyExclusive)}, + {name: `Invalid Address errors.New(IPv6CIDR_Error_Invalid)`, + input: CloudInitIPv6Config{Address: util.Pointer(IPv6CIDR("3f6d:5b2a:1e4d:7c91:abcd:1234:5678:9abc"))}, + output: errors.New(IPv6CIDR_Error_Invalid)}, + {name: `Invalid Gateway errors.New(IPv6Address_Error_Invalid)`, + input: CloudInitIPv6Config{Gateway: util.Pointer(IPv6Address("2001:0db8:85a3::/64"))}, + output: errors.New(IPv6Address_Error_Invalid)}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require.Equal(t, test.output, test.input.Validate()) + }) + } +} + +func Test_IPv4Address_Validate(t *testing.T) { + tests := []struct { + name string + input IPv4Address + output error + }{ + {name: `Valid`, + input: "192.168.45.1"}, + {name: "Valid empty"}, + {name: `Invalid is CIDR`, + input: "192.168.45.1/24", + output: errors.New(IPv4Address_Error_Invalid)}, + {name: `Invalid is IPv6`, + input: "3f6d:5b2a:1e4d:7c91:abcd:1234:5678:9abc", + output: errors.New(IPv4Address_Error_Invalid)}, + {name: `Invalid is gibberish`, + input: "ABCDEFG123", + output: errors.New(IPv4Address_Error_Invalid)}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require.Equal(t, test.output, test.input.Validate()) + }) + } +} + +func Test_IPv4CIDR_Validate(t *testing.T) { + tests := []struct { + name string + input IPv4CIDR + output error + }{ + {name: `Valid`, + input: "192.168.45.0/24"}, + {name: `Valid empty`}, + {name: `Invalid only IP no CIDR`, + input: "192.168.45.0", + output: errors.New(IPv4CIDR_Error_Invalid)}, + {name: `Invalid is IPv6`, + input: "2001:0db8:85a3::/64", + output: errors.New(IPv4CIDR_Error_Invalid)}, + {name: `Invalid gibberish`, + input: "ABCDEFG123", + output: errors.New(IPv4CIDR_Error_Invalid)}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require.Equal(t, test.output, test.input.Validate()) + }) + } +} + +func Test_IPv6Address_Validate(t *testing.T) { + tests := []struct { + name string + input IPv6Address + output error + }{ + {name: `Valid`, + input: "3f6d:5b2a:1e4d:7c91:abcd:1234:5678:9abc"}, + {name: `Valid empty`}, + {name: `Invalid is CIDR`, + input: "2001:0db8:85a3::/64", + output: errors.New(IPv6Address_Error_Invalid)}, + {name: `Invalid is IPv4`, + input: "192.168.45.0", + output: errors.New(IPv6Address_Error_Invalid)}, + {name: `Invalid is gibberish`, + input: "ABCDEFG123", + output: errors.New(IPv6Address_Error_Invalid)}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require.Equal(t, test.output, test.input.Validate()) + }) + } +} + +func Test_IPv6CIDR_Validate(t *testing.T) { + tests := []struct { + name string + input IPv6CIDR + output error + }{ + {name: `Valid`, + input: "2001:0db8:85a3::/64"}, + {name: `Valid empty`}, + {name: `Invalid only IP no CIDR`, + input: "2001:0db8:85a3::", + output: errors.New(IPv6CIDR_Error_Invalid)}, + {name: `Invalid is IPv4`, + input: "192.168.45.0/24", + output: errors.New(IPv6CIDR_Error_Invalid)}, + {name: `Invalid gibberish`, + input: "ABCDEFG123", + output: errors.New(IPv6CIDR_Error_Invalid)}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require.Equal(t, test.output, test.input.Validate()) + }) + } +} diff --git a/proxmox/config_qemu_test.go b/proxmox/config_qemu_test.go index d2de5ef8..f12026c4 100644 --- a/proxmox/config_qemu_test.go +++ b/proxmox/config_qemu_test.go @@ -1,8 +1,9 @@ package proxmox import ( + "crypto" "errors" - "strings" + "net/netip" "testing" "github.com/Telmate/proxmox-api-go/internal/util" @@ -13,6 +14,34 @@ import ( ) func Test_ConfigQemu_mapToApiValues(t *testing.T) { + cloudInitCustom := func() *CloudInitCustom { + return &CloudInitCustom{ + Meta: &CloudInitSnippet{ + Storage: "local-zfs", + FilePath: "ci-meta.yml"}, + Network: &CloudInitSnippet{ + Storage: "local-lvm", + FilePath: "ci-network.yml"}, + User: &CloudInitSnippet{ + Storage: "folder", + FilePath: "ci-user.yml"}, + Vendor: &CloudInitSnippet{ + Storage: "local", + FilePath: "snippets/ci-custom.yml"}} + } + cloudInitNetworkConfig := func() CloudInitNetworkConfig { + return CloudInitNetworkConfig{ + IPv4: &CloudInitIPv4Config{ + Address: util.Pointer(IPv4CIDR("192.168.56.30/24")), + Gateway: util.Pointer(IPv4Address("192.168.56.1"))}, + IPv6: &CloudInitIPv6Config{ + Address: util.Pointer(IPv6CIDR("2001:0db8:abcd::/48")), + Gateway: util.Pointer(IPv6Address("2001:0db8:abcd::1"))}} + } + parseIP := func(rawIP string) (ip netip.Addr) { + ip, _ = netip.ParseAddr(rawIP) + return + } format_Raw := QemuDiskFormat_Raw float10 := QemuDiskBandwidthMBpsLimitConcurrent(10.3) float45 := QemuDiskBandwidthMBpsLimitConcurrent(45.23) @@ -63,6 +92,116 @@ func Test_ConfigQemu_mapToApiValues(t *testing.T) { {name: `Create Agent.FsTrim`, config: &ConfigQemu{Agent: &QemuGuestAgent{FsTrim: util.Pointer(true)}}, output: map[string]interface{}{"agent": "0,fstrim_cloned_disks=1"}}, + // Create CloudInit no need for update as update and create behave the same. will be changed in the future + {name: `Create CloudInit=nil`, + config: &ConfigQemu{}, + output: map[string]interface{}{}}, + {name: `Create CloudInit Full`, + config: &ConfigQemu{CloudInit: &CloudInit{ + Custom: &CloudInitCustom{ + Meta: &CloudInitSnippet{ + Storage: "local-zfs", + FilePath: "ci-meta.yml"}, + Network: &CloudInitSnippet{ + Storage: "local-lvm", + FilePath: "ci-network.yml"}, + User: &CloudInitSnippet{ + Storage: "folder", + FilePath: "ci-user.yml"}, + Vendor: &CloudInitSnippet{ + Storage: "local", + FilePath: "snippets/ci-custom.yml"}}, + DNS: &GuestDNS{ + SearchDomain: util.Pointer("example.com"), + NameServers: &[]netip.Addr{parseIP("1.1.1.1"), parseIP("8.8.8.8"), parseIP("9.9.9.9")}}, + NetworkInterfaces: CloudInitNetworkInterfaces{ + QemuNetworkInterfaceID0: CloudInitNetworkConfig{ + IPv4: &CloudInitIPv4Config{DHCP: true}, + IPv6: &CloudInitIPv6Config{DHCP: true}}, + QemuNetworkInterfaceID19: CloudInitNetworkConfig{}, + QemuNetworkInterfaceID31: CloudInitNetworkConfig{ + IPv4: &CloudInitIPv4Config{Address: util.Pointer(IPv4CIDR("10.20.4.7/22"))}}}, + PublicSSHkeys: util.Pointer(test_data_qemu.PublicKey_Decoded_Input()), + UpgradePackages: util.Pointer(true), + UserPassword: util.Pointer("Enter123!"), + Username: util.Pointer("root")}}, + output: map[string]interface{}{ + "cicustom": "meta=local-zfs:ci-meta.yml,network=local-lvm:ci-network.yml,user=folder:ci-user.yml,vendor=local:snippets/ci-custom.yml", + "searchdomain": "example.com", + "nameserver": "1.1.1.1 8.8.8.8 9.9.9.9", + "ipconfig0": "ip=dhcp,ip6=dhcp", + "ipconfig31": "ip=10.20.4.7/22", + "sshkeys": test_data_qemu.PublicKey_Encoded_Output(), + "ciupgrade": 1, + "cipassword": "Enter123!", + "ciuser": "root"}}, + {name: `Create CloudInit Custom Network`, + config: &ConfigQemu{CloudInit: &CloudInit{Custom: &CloudInitCustom{ + Network: &CloudInitSnippet{ + Storage: "local", + FilePath: "ci-network.yml"}}}}, + output: map[string]interface{}{"cicustom": "network=local:ci-network.yml"}}, + {name: `Create CloudInit Custom User`, + config: &ConfigQemu{CloudInit: &CloudInit{Custom: &CloudInitCustom{ + User: &CloudInitSnippet{ + Storage: "file", + FilePath: "abcd.yml"}}}}, + output: map[string]interface{}{"cicustom": "user=file:abcd.yml"}}, + {name: `Create CloudInit Custom Vendor`, + config: &ConfigQemu{CloudInit: &CloudInit{Custom: &CloudInitCustom{ + Vendor: &CloudInitSnippet{ + Storage: "local", + FilePath: "vendor-ci"}}}}, + output: map[string]interface{}{"cicustom": "vendor=local:vendor-ci"}}, + {name: `Create CloudInit Custom Meta`, + config: &ConfigQemu{CloudInit: &CloudInit{Custom: &CloudInitCustom{ + Meta: &CloudInitSnippet{ + Storage: "local-zfs", + FilePath: "ci-meta.yml"}}}}, + output: map[string]interface{}{"cicustom": "meta=local-zfs:ci-meta.yml"}}, + {name: `Create CloudInit DNS NameServers`, + config: &ConfigQemu{CloudInit: &CloudInit{DNS: &GuestDNS{ + NameServers: &[]netip.Addr{parseIP("9.9.9.9")}}}}, + output: map[string]interface{}{"nameserver": "9.9.9.9"}}, + {name: `Create CloudInit DNS NameServers empty`, + config: &ConfigQemu{CloudInit: &CloudInit{DNS: &GuestDNS{ + NameServers: &[]netip.Addr{}}}}, + output: map[string]interface{}{}}, + {name: `Create CloudInit DNS SearchDomain`, + config: &ConfigQemu{CloudInit: &CloudInit{DNS: &GuestDNS{SearchDomain: util.Pointer("example.com")}}}, + output: map[string]interface{}{"searchdomain": "example.com"}}, + {name: `Create CloudInit DNS SearchDomain empty`, + config: &ConfigQemu{CloudInit: &CloudInit{DNS: &GuestDNS{SearchDomain: util.Pointer("")}}}, + output: map[string]interface{}{}}, + {name: `Create CloudInit NetworkInterfaces`, + config: &ConfigQemu{CloudInit: &CloudInit{NetworkInterfaces: CloudInitNetworkInterfaces{ + QemuNetworkInterfaceID1: CloudInitNetworkConfig{ + IPv4: &CloudInitIPv4Config{DHCP: true}, + IPv6: &CloudInitIPv6Config{DHCP: true}}, + QemuNetworkInterfaceID20: CloudInitNetworkConfig{}, + QemuNetworkInterfaceID30: CloudInitNetworkConfig{ + IPv4: &CloudInitIPv4Config{Address: util.Pointer(IPv4CIDR("10.20.4.7/22"))}}}}}, + output: map[string]interface{}{ + "ipconfig1": "ip=dhcp,ip6=dhcp", + "ipconfig30": "ip=10.20.4.7/22"}}, + {name: `Create CloudInit PublicSSHkeys`, + config: &ConfigQemu{CloudInit: &CloudInit{PublicSSHkeys: util.Pointer(test_data_qemu.PublicKey_Decoded_Input())}}, + output: map[string]interface{}{"sshkeys": test_data_qemu.PublicKey_Encoded_Output()}}, + {name: `Create CloudInit PublicSSHkeys empty`, + config: &ConfigQemu{CloudInit: &CloudInit{PublicSSHkeys: util.Pointer([]crypto.PublicKey{})}}, + output: map[string]interface{}{}}, + {name: `Create CloudInit UpgradePackages`, + config: &ConfigQemu{CloudInit: &CloudInit{UpgradePackages: util.Pointer(false)}}, + output: map[string]interface{}{"ciupgrade": 0}}, + {name: `Create CloudInit UserPassword`, + config: &ConfigQemu{CloudInit: &CloudInit{UserPassword: util.Pointer("Enter123!")}}, + output: map[string]interface{}{"cipassword": "Enter123!"}}, + {name: `Create CloudInit Username`, + config: &ConfigQemu{CloudInit: &CloudInit{Username: util.Pointer("root")}}, + output: map[string]interface{}{"ciuser": "root"}}, + {name: `Create CloudInit Username empty`, + config: &ConfigQemu{CloudInit: &CloudInit{Username: util.Pointer("")}}, + output: map[string]interface{}{}}, // Create Disks // Create Disks.Ide @@ -1400,6 +1539,305 @@ func Test_ConfigQemu_mapToApiValues(t *testing.T) { config: &ConfigQemu{Agent: &QemuGuestAgent{}}, currentConfig: ConfigQemu{Agent: &QemuGuestAgent{}}, output: map[string]interface{}{"agent": "0"}}, + // Update CloudInit + {name: `Update CloudInit=nil`, + config: &ConfigQemu{}, + output: map[string]interface{}{}}, + {name: `Update CloudInit Custom clear`, + config: &ConfigQemu{CloudInit: &CloudInit{Custom: &CloudInitCustom{ + Meta: &CloudInitSnippet{}, + Network: &CloudInitSnippet{}, + User: &CloudInitSnippet{}, + Vendor: &CloudInitSnippet{}}}}, + currentConfig: ConfigQemu{CloudInit: &CloudInit{Custom: cloudInitCustom()}}, + output: map[string]interface{}{"cicustom": ""}}, + {name: `Update CloudInit Custom Network`, + config: &ConfigQemu{CloudInit: &CloudInit{Custom: &CloudInitCustom{ + Network: &CloudInitSnippet{ + Storage: "newStorage", + FilePath: "new.yml"}}}}, + currentConfig: ConfigQemu{CloudInit: &CloudInit{Custom: cloudInitCustom()}}, + output: map[string]interface{}{"cicustom": "meta=local-zfs:ci-meta.yml,network=newStorage:new.yml,user=folder:ci-user.yml,vendor=local:snippets/ci-custom.yml"}}, + {name: `Update CloudInit Custom User`, + config: &ConfigQemu{CloudInit: &CloudInit{Custom: &CloudInitCustom{ + User: &CloudInitSnippet{ + Storage: "newStorage", + FilePath: "new.yml"}}}}, + currentConfig: ConfigQemu{CloudInit: &CloudInit{Custom: cloudInitCustom()}}, + output: map[string]interface{}{"cicustom": "meta=local-zfs:ci-meta.yml,network=local-lvm:ci-network.yml,user=newStorage:new.yml,vendor=local:snippets/ci-custom.yml"}}, + {name: `Update CloudInit Custom Vendor`, + config: &ConfigQemu{CloudInit: &CloudInit{Custom: &CloudInitCustom{ + Vendor: &CloudInitSnippet{ + Storage: "newStorage", + FilePath: "new.yml"}}}}, + currentConfig: ConfigQemu{CloudInit: &CloudInit{Custom: cloudInitCustom()}}, + output: map[string]interface{}{"cicustom": "meta=local-zfs:ci-meta.yml,network=local-lvm:ci-network.yml,user=folder:ci-user.yml,vendor=newStorage:new.yml"}}, + {name: `Update CloudInit Custom Meta`, + config: &ConfigQemu{CloudInit: &CloudInit{Custom: &CloudInitCustom{ + Meta: &CloudInitSnippet{ + Storage: "newStorage", + FilePath: "new.yml"}}}}, + currentConfig: ConfigQemu{CloudInit: &CloudInit{Custom: cloudInitCustom()}}, + output: map[string]interface{}{"cicustom": "meta=newStorage:new.yml,network=local-lvm:ci-network.yml,user=folder:ci-user.yml,vendor=local:snippets/ci-custom.yml"}}, + {name: `Update CloudInit DNS NameServers`, + config: &ConfigQemu{CloudInit: &CloudInit{DNS: &GuestDNS{ + NameServers: &[]netip.Addr{parseIP("9.9.9.9")}}}}, + currentConfig: ConfigQemu{CloudInit: &CloudInit{DNS: &GuestDNS{ + NameServers: &[]netip.Addr{parseIP("8.8.8.8")}}}}, + output: map[string]interface{}{"nameserver": "9.9.9.9"}}, + {name: `Update CloudInit DNS NameServers empty`, + config: &ConfigQemu{CloudInit: &CloudInit{DNS: &GuestDNS{ + NameServers: &[]netip.Addr{}}}}, + currentConfig: ConfigQemu{CloudInit: &CloudInit{DNS: &GuestDNS{ + NameServers: &[]netip.Addr{parseIP("8.8.8.8")}}}}, + output: map[string]interface{}{"delete": "nameserver"}}, + {name: `Update CloudInit DNS SearchDomain`, + config: &ConfigQemu{CloudInit: &CloudInit{DNS: &GuestDNS{SearchDomain: util.Pointer("example.com")}}}, + currentConfig: ConfigQemu{CloudInit: &CloudInit{DNS: &GuestDNS{SearchDomain: util.Pointer("example.org")}}}, + output: map[string]interface{}{"searchdomain": "example.com"}}, + {name: `Update CloudInit DNS SearchDomain empty`, + config: &ConfigQemu{CloudInit: &CloudInit{DNS: &GuestDNS{SearchDomain: util.Pointer("")}}}, + currentConfig: ConfigQemu{CloudInit: &CloudInit{DNS: &GuestDNS{SearchDomain: util.Pointer("example.org")}}}, + output: map[string]interface{}{"delete": "searchdomain"}}, + {name: `Update CloudInit NetworkInterfaces Ipv4.Address update`, + config: &ConfigQemu{CloudInit: &CloudInit{NetworkInterfaces: CloudInitNetworkInterfaces{ + QemuNetworkInterfaceID0: CloudInitNetworkConfig{ + IPv4: &CloudInitIPv4Config{Address: util.Pointer(IPv4CIDR("192.168.1.10/24"))}}}}}, + currentConfig: ConfigQemu{CloudInit: &CloudInit{NetworkInterfaces: CloudInitNetworkInterfaces{ + QemuNetworkInterfaceID0: cloudInitNetworkConfig()}}}, + output: map[string]interface{}{"ipconfig0": "ip=192.168.1.10/24,gw=192.168.56.1,ip6=2001:0db8:abcd::/48,gw6=2001:0db8:abcd::1"}}, + {name: `Update CloudInit NetworkInterfaces Ipv4.Address remove`, + config: &ConfigQemu{CloudInit: &CloudInit{NetworkInterfaces: CloudInitNetworkInterfaces{ + QemuNetworkInterfaceID1: CloudInitNetworkConfig{ + IPv4: &CloudInitIPv4Config{Address: util.Pointer(IPv4CIDR(""))}}}}}, + currentConfig: ConfigQemu{CloudInit: &CloudInit{NetworkInterfaces: CloudInitNetworkInterfaces{ + QemuNetworkInterfaceID1: cloudInitNetworkConfig()}}}, + output: map[string]interface{}{"ipconfig1": "gw=192.168.56.1,ip6=2001:0db8:abcd::/48,gw6=2001:0db8:abcd::1"}}, + {name: `Update CloudInit NetworkInterfaces Ipv4.DHCP set`, + config: &ConfigQemu{CloudInit: &CloudInit{NetworkInterfaces: CloudInitNetworkInterfaces{ + QemuNetworkInterfaceID2: CloudInitNetworkConfig{ + IPv4: &CloudInitIPv4Config{DHCP: true}}}}}, + currentConfig: ConfigQemu{CloudInit: &CloudInit{NetworkInterfaces: CloudInitNetworkInterfaces{ + QemuNetworkInterfaceID2: cloudInitNetworkConfig()}}}, + output: map[string]interface{}{"ipconfig2": "ip=dhcp,ip6=2001:0db8:abcd::/48,gw6=2001:0db8:abcd::1"}}, + {name: `Update CloudInit NetworkInterfaces Ipv4.Gateway update`, + config: &ConfigQemu{CloudInit: &CloudInit{NetworkInterfaces: CloudInitNetworkInterfaces{ + QemuNetworkInterfaceID3: CloudInitNetworkConfig{ + IPv4: &CloudInitIPv4Config{Gateway: util.Pointer(IPv4Address("192.168.1.1"))}}}}}, + currentConfig: ConfigQemu{CloudInit: &CloudInit{NetworkInterfaces: CloudInitNetworkInterfaces{ + QemuNetworkInterfaceID3: cloudInitNetworkConfig()}}}, + output: map[string]interface{}{"ipconfig3": "ip=192.168.56.30/24,gw=192.168.1.1,ip6=2001:0db8:abcd::/48,gw6=2001:0db8:abcd::1"}}, + {name: `Update CloudInit NetworkInterfaces Ipv4.Gateway overwrite Ipv4.DHCP`, + config: &ConfigQemu{CloudInit: &CloudInit{NetworkInterfaces: CloudInitNetworkInterfaces{ + QemuNetworkInterfaceID4: CloudInitNetworkConfig{ + IPv4: &CloudInitIPv4Config{Gateway: util.Pointer(IPv4Address("192.168.1.1"))}}}}}, + currentConfig: ConfigQemu{CloudInit: &CloudInit{NetworkInterfaces: CloudInitNetworkInterfaces{ + QemuNetworkInterfaceID4: CloudInitNetworkConfig{ + IPv4: &CloudInitIPv4Config{DHCP: true}}}}}, + output: map[string]interface{}{"ipconfig4": "gw=192.168.1.1"}}, + {name: `Update CloudInit NetworkInterfaces Ipv4.Gateway remove`, + config: &ConfigQemu{CloudInit: &CloudInit{NetworkInterfaces: CloudInitNetworkInterfaces{ + QemuNetworkInterfaceID5: CloudInitNetworkConfig{ + IPv4: &CloudInitIPv4Config{Gateway: util.Pointer(IPv4Address(""))}}}}}, + currentConfig: ConfigQemu{CloudInit: &CloudInit{NetworkInterfaces: CloudInitNetworkInterfaces{ + QemuNetworkInterfaceID5: cloudInitNetworkConfig()}}}, + output: map[string]interface{}{"ipconfig5": "ip=192.168.56.30/24,ip6=2001:0db8:abcd::/48,gw6=2001:0db8:abcd::1"}}, + {name: `Update CloudInit NetworkInterfaces Ipv6.Address update`, + config: &ConfigQemu{CloudInit: &CloudInit{NetworkInterfaces: CloudInitNetworkInterfaces{ + QemuNetworkInterfaceID6: CloudInitNetworkConfig{ + IPv6: &CloudInitIPv6Config{Address: util.Pointer(IPv6CIDR("2001:0db8:85a3::/48"))}}}}}, + currentConfig: ConfigQemu{CloudInit: &CloudInit{NetworkInterfaces: CloudInitNetworkInterfaces{ + QemuNetworkInterfaceID6: cloudInitNetworkConfig()}}}, + output: map[string]interface{}{"ipconfig6": "ip=192.168.56.30/24,gw=192.168.56.1,ip6=2001:0db8:85a3::/48,gw6=2001:0db8:abcd::1"}}, + {name: `Update CloudInit NetworkInterfaces Ipv6.Address remove`, + config: &ConfigQemu{CloudInit: &CloudInit{NetworkInterfaces: CloudInitNetworkInterfaces{ + QemuNetworkInterfaceID7: CloudInitNetworkConfig{ + IPv6: &CloudInitIPv6Config{Address: util.Pointer(IPv6CIDR(""))}}}}}, + currentConfig: ConfigQemu{CloudInit: &CloudInit{NetworkInterfaces: CloudInitNetworkInterfaces{ + QemuNetworkInterfaceID7: cloudInitNetworkConfig()}}}, + output: map[string]interface{}{"ipconfig7": "ip=192.168.56.30/24,gw=192.168.56.1,gw6=2001:0db8:abcd::1"}}, + {name: `Update CloudInit NetworkInterfaces Ipv6.DHCP set`, + config: &ConfigQemu{CloudInit: &CloudInit{NetworkInterfaces: CloudInitNetworkInterfaces{ + QemuNetworkInterfaceID8: CloudInitNetworkConfig{ + IPv6: &CloudInitIPv6Config{DHCP: true}}}}}, + currentConfig: ConfigQemu{CloudInit: &CloudInit{NetworkInterfaces: CloudInitNetworkInterfaces{ + QemuNetworkInterfaceID8: cloudInitNetworkConfig()}}}, + output: map[string]interface{}{"ipconfig8": "ip=192.168.56.30/24,gw=192.168.56.1,ip6=dhcp"}}, + {name: `Update CloudInit NetworkInterfaces Ipv6.Gateway update`, + config: &ConfigQemu{CloudInit: &CloudInit{NetworkInterfaces: CloudInitNetworkInterfaces{ + QemuNetworkInterfaceID9: CloudInitNetworkConfig{ + IPv6: &CloudInitIPv6Config{Gateway: util.Pointer(IPv6Address("2001:0db8:85a3::1"))}}}}}, + currentConfig: ConfigQemu{CloudInit: &CloudInit{NetworkInterfaces: CloudInitNetworkInterfaces{ + QemuNetworkInterfaceID9: cloudInitNetworkConfig()}}}, + output: map[string]interface{}{"ipconfig9": "ip=192.168.56.30/24,gw=192.168.56.1,ip6=2001:0db8:abcd::/48,gw6=2001:0db8:85a3::1"}}, + {name: `Update CloudInit NetworkInterfaces Ipv6.Gateway overwrite Ipv6.DHCP`, + config: &ConfigQemu{CloudInit: &CloudInit{NetworkInterfaces: CloudInitNetworkInterfaces{ + QemuNetworkInterfaceID10: CloudInitNetworkConfig{ + IPv6: &CloudInitIPv6Config{Gateway: util.Pointer(IPv6Address("2001:0db8:85a3::1"))}}}}}, + currentConfig: ConfigQemu{CloudInit: &CloudInit{NetworkInterfaces: CloudInitNetworkInterfaces{ + QemuNetworkInterfaceID10: CloudInitNetworkConfig{ + IPv6: &CloudInitIPv6Config{DHCP: true}}}}}, + output: map[string]interface{}{"ipconfig10": "gw6=2001:0db8:85a3::1"}}, + {name: `Update CloudInit NetworkInterfaces Ipv6.Gateway overwrite Ipv6.SLAAC`, + config: &ConfigQemu{CloudInit: &CloudInit{NetworkInterfaces: CloudInitNetworkInterfaces{ + QemuNetworkInterfaceID11: CloudInitNetworkConfig{ + IPv6: &CloudInitIPv6Config{Gateway: util.Pointer(IPv6Address("2001:0db8:85a3::1"))}}}}}, + currentConfig: ConfigQemu{CloudInit: &CloudInit{NetworkInterfaces: CloudInitNetworkInterfaces{ + QemuNetworkInterfaceID11: CloudInitNetworkConfig{ + IPv6: &CloudInitIPv6Config{SLAAC: true}}}}}, + output: map[string]interface{}{"ipconfig11": "gw6=2001:0db8:85a3::1"}}, + {name: `Update CloudInit NetworkInterfaces Ipv6.Gateway remove`, + config: &ConfigQemu{CloudInit: &CloudInit{NetworkInterfaces: CloudInitNetworkInterfaces{ + QemuNetworkInterfaceID12: CloudInitNetworkConfig{ + IPv6: &CloudInitIPv6Config{Gateway: util.Pointer(IPv6Address(""))}}}}}, + currentConfig: ConfigQemu{CloudInit: &CloudInit{NetworkInterfaces: CloudInitNetworkInterfaces{ + QemuNetworkInterfaceID12: cloudInitNetworkConfig()}}}, + output: map[string]interface{}{"ipconfig12": "ip=192.168.56.30/24,gw=192.168.56.1,ip6=2001:0db8:abcd::/48"}}, + {name: `Update CloudInit NetworkInterfaces Ipv6.SLAAC set`, + config: &ConfigQemu{CloudInit: &CloudInit{NetworkInterfaces: CloudInitNetworkInterfaces{ + QemuNetworkInterfaceID13: CloudInitNetworkConfig{ + IPv6: &CloudInitIPv6Config{SLAAC: true}}}}}, + currentConfig: ConfigQemu{CloudInit: &CloudInit{NetworkInterfaces: CloudInitNetworkInterfaces{ + QemuNetworkInterfaceID13: cloudInitNetworkConfig()}}}, + output: map[string]interface{}{"ipconfig13": "ip=192.168.56.30/24,gw=192.168.56.1,ip6=auto"}}, + {name: `Update CloudInit NetworkInterfaces delete existing interface`, + config: &ConfigQemu{CloudInit: &CloudInit{NetworkInterfaces: CloudInitNetworkInterfaces{ + QemuNetworkInterfaceID14: CloudInitNetworkConfig{ + IPv4: &CloudInitIPv4Config{ + Address: util.Pointer(IPv4CIDR("")), + Gateway: util.Pointer(IPv4Address(""))}, + IPv6: &CloudInitIPv6Config{ + Address: util.Pointer(IPv6CIDR("")), + Gateway: util.Pointer(IPv6Address(""))}}}}}, + currentConfig: ConfigQemu{CloudInit: &CloudInit{NetworkInterfaces: CloudInitNetworkInterfaces{ + QemuNetworkInterfaceID14: cloudInitNetworkConfig()}}}, + output: map[string]interface{}{"delete": "ipconfig14"}}, + {name: `Update CloudInit NetworkInterfaces delete non-existing interface`, + config: &ConfigQemu{CloudInit: &CloudInit{NetworkInterfaces: CloudInitNetworkInterfaces{ + QemuNetworkInterfaceID20: CloudInitNetworkConfig{ + IPv4: &CloudInitIPv4Config{ + Address: util.Pointer(IPv4CIDR("")), + Gateway: util.Pointer(IPv4Address(""))}, + IPv6: &CloudInitIPv6Config{ + Address: util.Pointer(IPv6CIDR("")), + Gateway: util.Pointer(IPv6Address(""))}}}}}, + currentConfig: ConfigQemu{CloudInit: &CloudInit{NetworkInterfaces: CloudInitNetworkInterfaces{}}}, + output: map[string]interface{}{}}, + {name: `Update CloudInit NetworkInterfaces no updates`, + config: &ConfigQemu{CloudInit: &CloudInit{NetworkInterfaces: CloudInitNetworkInterfaces{}}}, + currentConfig: ConfigQemu{CloudInit: &CloudInit{NetworkInterfaces: CloudInitNetworkInterfaces{ + QemuNetworkInterfaceID29: cloudInitNetworkConfig(), + QemuNetworkInterfaceID30: CloudInitNetworkConfig{ + IPv4: &CloudInitIPv4Config{DHCP: true}, + IPv6: &CloudInitIPv6Config{DHCP: true}}}}}, + output: map[string]interface{}{}}, + {name: `Update CloudInit NetworkInterfaces full`, + config: &ConfigQemu{CloudInit: &CloudInit{NetworkInterfaces: CloudInitNetworkInterfaces{ + QemuNetworkInterfaceID0: CloudInitNetworkConfig{ + IPv4: &CloudInitIPv4Config{Address: util.Pointer(IPv4CIDR("192.168.1.10/24"))}}, + QemuNetworkInterfaceID1: CloudInitNetworkConfig{ + IPv4: &CloudInitIPv4Config{Address: util.Pointer(IPv4CIDR(""))}}, + QemuNetworkInterfaceID2: CloudInitNetworkConfig{ + IPv4: &CloudInitIPv4Config{DHCP: true}}, + QemuNetworkInterfaceID3: CloudInitNetworkConfig{ + IPv4: &CloudInitIPv4Config{Gateway: util.Pointer(IPv4Address("192.168.1.1"))}}, + QemuNetworkInterfaceID4: CloudInitNetworkConfig{ + IPv4: &CloudInitIPv4Config{Gateway: util.Pointer(IPv4Address("192.168.1.1"))}}, + QemuNetworkInterfaceID5: CloudInitNetworkConfig{ + IPv4: &CloudInitIPv4Config{Gateway: util.Pointer(IPv4Address(""))}}, + QemuNetworkInterfaceID6: CloudInitNetworkConfig{ + IPv6: &CloudInitIPv6Config{Address: util.Pointer(IPv6CIDR("2001:0db8:85a3::/48"))}}, + QemuNetworkInterfaceID7: CloudInitNetworkConfig{ + IPv6: &CloudInitIPv6Config{Address: util.Pointer(IPv6CIDR(""))}}, + QemuNetworkInterfaceID8: CloudInitNetworkConfig{ + IPv6: &CloudInitIPv6Config{DHCP: true}}, + QemuNetworkInterfaceID9: CloudInitNetworkConfig{ + IPv6: &CloudInitIPv6Config{Gateway: util.Pointer(IPv6Address("2001:0db8:85a3::1"))}}, + QemuNetworkInterfaceID10: CloudInitNetworkConfig{ + IPv6: &CloudInitIPv6Config{Gateway: util.Pointer(IPv6Address("2001:0db8:85a3::1"))}}, + QemuNetworkInterfaceID11: CloudInitNetworkConfig{ + IPv6: &CloudInitIPv6Config{Gateway: util.Pointer(IPv6Address("2001:0db8:85a3::1"))}}, + QemuNetworkInterfaceID12: CloudInitNetworkConfig{ + IPv6: &CloudInitIPv6Config{Gateway: util.Pointer(IPv6Address(""))}}, + QemuNetworkInterfaceID13: CloudInitNetworkConfig{ + IPv6: &CloudInitIPv6Config{SLAAC: true}}, + QemuNetworkInterfaceID14: CloudInitNetworkConfig{ + IPv4: &CloudInitIPv4Config{ + Address: util.Pointer(IPv4CIDR("")), + Gateway: util.Pointer(IPv4Address(""))}, + IPv6: &CloudInitIPv6Config{ + Address: util.Pointer(IPv6CIDR("")), + Gateway: util.Pointer(IPv6Address(""))}}, + QemuNetworkInterfaceID20: CloudInitNetworkConfig{ + IPv4: &CloudInitIPv4Config{ + Address: util.Pointer(IPv4CIDR("")), + Gateway: util.Pointer(IPv4Address(""))}, + IPv6: &CloudInitIPv6Config{ + Address: util.Pointer(IPv6CIDR("")), + Gateway: util.Pointer(IPv6Address(""))}}}}}, + currentConfig: ConfigQemu{CloudInit: &CloudInit{NetworkInterfaces: CloudInitNetworkInterfaces{ + QemuNetworkInterfaceID0: cloudInitNetworkConfig(), + QemuNetworkInterfaceID1: cloudInitNetworkConfig(), + QemuNetworkInterfaceID2: cloudInitNetworkConfig(), + QemuNetworkInterfaceID3: cloudInitNetworkConfig(), + QemuNetworkInterfaceID4: CloudInitNetworkConfig{ + IPv4: &CloudInitIPv4Config{DHCP: true}}, + QemuNetworkInterfaceID5: cloudInitNetworkConfig(), + QemuNetworkInterfaceID6: cloudInitNetworkConfig(), + QemuNetworkInterfaceID7: cloudInitNetworkConfig(), + QemuNetworkInterfaceID8: cloudInitNetworkConfig(), + QemuNetworkInterfaceID9: cloudInitNetworkConfig(), + QemuNetworkInterfaceID10: CloudInitNetworkConfig{ + IPv6: &CloudInitIPv6Config{DHCP: true}}, + QemuNetworkInterfaceID11: CloudInitNetworkConfig{ + IPv6: &CloudInitIPv6Config{SLAAC: true}}, + QemuNetworkInterfaceID12: cloudInitNetworkConfig(), + QemuNetworkInterfaceID13: cloudInitNetworkConfig(), + QemuNetworkInterfaceID14: cloudInitNetworkConfig(), + QemuNetworkInterfaceID29: cloudInitNetworkConfig(), + QemuNetworkInterfaceID30: CloudInitNetworkConfig{ + IPv4: &CloudInitIPv4Config{DHCP: true}, + IPv6: &CloudInitIPv6Config{DHCP: true}}}}}, + output: map[string]interface{}{ + "ipconfig0": "ip=192.168.1.10/24,gw=192.168.56.1,ip6=2001:0db8:abcd::/48,gw6=2001:0db8:abcd::1", + "ipconfig1": "gw=192.168.56.1,ip6=2001:0db8:abcd::/48,gw6=2001:0db8:abcd::1", + "ipconfig2": "ip=dhcp,ip6=2001:0db8:abcd::/48,gw6=2001:0db8:abcd::1", + "ipconfig3": "ip=192.168.56.30/24,gw=192.168.1.1,ip6=2001:0db8:abcd::/48,gw6=2001:0db8:abcd::1", + "ipconfig4": "gw=192.168.1.1", + "ipconfig5": "ip=192.168.56.30/24,ip6=2001:0db8:abcd::/48,gw6=2001:0db8:abcd::1", + "ipconfig6": "ip=192.168.56.30/24,gw=192.168.56.1,ip6=2001:0db8:85a3::/48,gw6=2001:0db8:abcd::1", + "ipconfig7": "ip=192.168.56.30/24,gw=192.168.56.1,gw6=2001:0db8:abcd::1", + "ipconfig8": "ip=192.168.56.30/24,gw=192.168.56.1,ip6=dhcp", + "ipconfig9": "ip=192.168.56.30/24,gw=192.168.56.1,ip6=2001:0db8:abcd::/48,gw6=2001:0db8:85a3::1", + "ipconfig10": "gw6=2001:0db8:85a3::1", + "ipconfig11": "gw6=2001:0db8:85a3::1", + "ipconfig12": "ip=192.168.56.30/24,gw=192.168.56.1,ip6=2001:0db8:abcd::/48", + "ipconfig13": "ip=192.168.56.30/24,gw=192.168.56.1,ip6=auto", + "delete": "ipconfig14"}}, + {name: `Update CloudInit PublicSSHkeys`, + config: &ConfigQemu{CloudInit: &CloudInit{PublicSSHkeys: util.Pointer(test_data_qemu.PublicKey_Decoded_Input())}}, + currentConfig: ConfigQemu{CloudInit: &CloudInit{PublicSSHkeys: util.Pointer([]crypto.PublicKey{"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC+0roY6F4yzq5RfA6V2+8gOgKlLOg9RtB1uGyTYvOMU6wxWUXVZP44+XozNxXZK4/MfPjCZLomqv78RlAedIQbqU8l6J9fdrrsRt6NknusE36UqD4HGPLX3Wn7svjSyNRfrjlk5BrBQ26rglLGlRSeD/xWvQ+5jLzzdo5NczszGkE9IQtrmKye7Gq7NQeGkHb1h0yGH7nMQ48WJ6ZKv1JG+GzFb8n4Qoei3zK9zpWxF+0AzF5u/zzCRZ4yU7FtfHgGRBDPze8oe3nVe+aO8MBH2dy8G/BRMXBdjWrSkaT9ZyeaT0k9SMjsCr9DQzUtVSOeqZZokpNU1dVglI+HU0vN test-key"})}}, + output: map[string]interface{}{"sshkeys": test_data_qemu.PublicKey_Encoded_Output()}}, + {name: `Update CloudInit PublicSSHkeys empty`, + config: &ConfigQemu{CloudInit: &CloudInit{PublicSSHkeys: util.Pointer([]crypto.PublicKey{})}}, + currentConfig: ConfigQemu{CloudInit: &CloudInit{PublicSSHkeys: util.Pointer([]crypto.PublicKey{"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC+0roY6F4yzq5RfA6V2+8gOgKlLOg9RtB1uGyTYvOMU6wxWUXVZP44+XozNxXZK4/MfPjCZLomqv78RlAedIQbqU8l6J9fdrrsRt6NknusE36UqD4HGPLX3Wn7svjSyNRfrjlk5BrBQ26rglLGlRSeD/xWvQ+5jLzzdo5NczszGkE9IQtrmKye7Gq7NQeGkHb1h0yGH7nMQ48WJ6ZKv1JG+GzFb8n4Qoei3zK9zpWxF+0AzF5u/zzCRZ4yU7FtfHgGRBDPze8oe3nVe+aO8MBH2dy8G/BRMXBdjWrSkaT9ZyeaT0k9SMjsCr9DQzUtVSOeqZZokpNU1dVglI+HU0vN test-key"})}}, + output: map[string]interface{}{"delete": "sshkeys"}}, + {name: `Update CloudInit UpgradePackages`, + config: &ConfigQemu{CloudInit: &CloudInit{UpgradePackages: util.Pointer(false)}}, + currentConfig: ConfigQemu{CloudInit: &CloudInit{UpgradePackages: util.Pointer(true)}}, + output: map[string]interface{}{"ciupgrade": 0}}, + {name: `Update CloudInit UserPassword`, + config: &ConfigQemu{CloudInit: &CloudInit{UserPassword: util.Pointer("Enter123!")}}, + currentConfig: ConfigQemu{CloudInit: &CloudInit{UserPassword: util.Pointer("Abc123!")}}, + output: map[string]interface{}{"cipassword": "Enter123!"}}, + {name: `Update CloudInit Username`, + config: &ConfigQemu{CloudInit: &CloudInit{Username: util.Pointer("root")}}, + currentConfig: ConfigQemu{CloudInit: &CloudInit{Username: util.Pointer("admin")}}, + output: map[string]interface{}{"ciuser": "root"}}, + {name: `Update CloudInit Username empty`, + config: &ConfigQemu{CloudInit: &CloudInit{Username: util.Pointer("")}}, + currentConfig: ConfigQemu{CloudInit: &CloudInit{Username: util.Pointer("admin")}}, + output: map[string]interface{}{"delete": "ciuser"}}, // Update Disk // Update Disk.Ide {name: "Update Disk.Ide.Disk_X DELETE", @@ -3403,6 +3841,10 @@ func Test_ConfigQemu_mapToApiValues(t *testing.T) { } func Test_ConfigQemu_mapToStruct(t *testing.T) { + parseIP := func(rawIP string) (ip netip.Addr) { + ip, _ = netip.ParseAddr(rawIP) + return + } uint1 := uint(1) uint2 := uint(2) uint31 := uint(31) @@ -3436,6 +3878,132 @@ func Test_ConfigQemu_mapToStruct(t *testing.T) { {name: `Agent Type`, input: map[string]interface{}{"agent": string("1,type=virtio")}, output: &ConfigQemu{Agent: &QemuGuestAgent{Enable: util.Pointer(true), Type: util.Pointer(QemuGuestAgentType_VirtIO)}}}, + // CloudInit + {name: `CloudInit ALL`, + input: map[string]interface{}{ + "cicustom": string("meta=local-zfs:ci-meta.yml,network=local-lvm:ci-network.yml,user=folder:ci-user.yml,vendor=local:snippets/ci-custom.yml"), + "searchdomain": string("example.com"), + "nameserver": string("1.1.1.1 8.8.8.8 9.9.9.9"), + "ipconfig0": string("ip=dhcp,ip6=dhcp"), + "ipconfig19": string(""), + "ipconfig31": string("ip=10.20.4.7/22"), + "sshkeys": test_data_qemu.PublicKey_Encoded_Input(), + "ciupgrade": float64(1), + "cipassword": string("Enter123!"), + "ciuser": string("root")}, + output: &ConfigQemu{CloudInit: &CloudInit{ + Custom: &CloudInitCustom{ + Meta: &CloudInitSnippet{ + FilePath: "ci-meta.yml", + Storage: "local-zfs"}, + Network: &CloudInitSnippet{ + FilePath: "ci-network.yml", + Storage: "local-lvm"}, + User: &CloudInitSnippet{ + FilePath: "ci-user.yml", + Storage: "folder"}, + Vendor: &CloudInitSnippet{ + FilePath: "snippets/ci-custom.yml", + Storage: "local"}}, + DNS: &GuestDNS{ + SearchDomain: util.Pointer("example.com"), + NameServers: &[]netip.Addr{parseIP("1.1.1.1"), parseIP("8.8.8.8"), parseIP("9.9.9.9")}}, + NetworkInterfaces: CloudInitNetworkInterfaces{ + QemuNetworkInterfaceID0: CloudInitNetworkConfig{ + IPv4: &CloudInitIPv4Config{DHCP: true}, + IPv6: &CloudInitIPv6Config{DHCP: true}}, + QemuNetworkInterfaceID31: CloudInitNetworkConfig{ + IPv4: &CloudInitIPv4Config{Address: util.Pointer(IPv4CIDR("10.20.4.7/22"))}}}, + PublicSSHkeys: util.Pointer(test_data_qemu.PublicKey_Decoded_Output()), + UpgradePackages: util.Pointer(true), + UserPassword: util.Pointer("Enter123!"), + Username: util.Pointer("root")}}}, + {name: `CloudInit Custom Meta`, + input: map[string]interface{}{"cicustom": string("meta=local-zfs:ci-meta.yml")}, + output: &ConfigQemu{CloudInit: &CloudInit{ + Custom: &CloudInitCustom{Meta: &CloudInitSnippet{FilePath: "ci-meta.yml", Storage: "local-zfs"}}, + NetworkInterfaces: CloudInitNetworkInterfaces{}}}}, + {name: `CloudInit Custom Network`, + input: map[string]interface{}{"cicustom": string("network=local-lvm:ci-network.yml")}, + output: &ConfigQemu{CloudInit: &CloudInit{ + Custom: &CloudInitCustom{Network: &CloudInitSnippet{FilePath: "ci-network.yml", Storage: "local-lvm"}}, + NetworkInterfaces: CloudInitNetworkInterfaces{}}}}, + {name: `CloudInit Custom User`, + input: map[string]interface{}{ + "cicustom": string("user=folder:ci-user.yml")}, + output: &ConfigQemu{CloudInit: &CloudInit{ + Custom: &CloudInitCustom{User: &CloudInitSnippet{FilePath: "ci-user.yml", Storage: "folder"}}, + NetworkInterfaces: CloudInitNetworkInterfaces{}}}}, + {name: `CloudInit Custom Vendor`, + input: map[string]interface{}{"cicustom": string("vendor=local:snippets/ci-custom.yml")}, + output: &ConfigQemu{CloudInit: &CloudInit{ + Custom: &CloudInitCustom{Vendor: &CloudInitSnippet{FilePath: "snippets/ci-custom.yml", Storage: "local"}}, + NetworkInterfaces: CloudInitNetworkInterfaces{}}}}, + {name: `CloudInit DNS SearchDomain`, + input: map[string]interface{}{"searchdomain": string("example.com")}, + output: &ConfigQemu{CloudInit: &CloudInit{ + DNS: &GuestDNS{ + SearchDomain: util.Pointer("example.com"), + NameServers: util.Pointer(uninitializedArray[netip.Addr]())}, + NetworkInterfaces: CloudInitNetworkInterfaces{}}}}, + {name: `CloudInit DNS SearchDomain empty`, + input: map[string]interface{}{"searchdomain": string(" ")}, + output: &ConfigQemu{}}, + {name: `CloudInit DNS NameServers`, + input: map[string]interface{}{"nameserver": string("1.1.1.1 8.8.8.8 9.9.9.9")}, + output: &ConfigQemu{CloudInit: &CloudInit{ + DNS: &GuestDNS{ + SearchDomain: util.Pointer(""), + NameServers: &[]netip.Addr{parseIP("1.1.1.1"), parseIP("8.8.8.8"), parseIP("9.9.9.9")}}, + NetworkInterfaces: CloudInitNetworkInterfaces{}}}}, + {name: `CloudInit NetworkInterfaces`, + input: map[string]interface{}{ + + "ipconfig0": string("ip=dhcp,ip6=dhcp"), + "ipconfig1": string("ip6=auto"), + "ipconfig2": string("ip=192.168.1.10/24,gw=192.168.56.1,ip6=2001:0db8:abcd::/48,gw6=2001:0db8:abcd::1"), + "ipconfig19": string(""), + "ipconfig20": string(" "), // this single space is on porpuse to test if it is ignored + "ipconfig31": string("ip=10.20.4.7/22")}, + output: &ConfigQemu{CloudInit: &CloudInit{ + NetworkInterfaces: CloudInitNetworkInterfaces{ + QemuNetworkInterfaceID0: CloudInitNetworkConfig{ + IPv4: &CloudInitIPv4Config{DHCP: true}, + IPv6: &CloudInitIPv6Config{DHCP: true}}, + QemuNetworkInterfaceID1: CloudInitNetworkConfig{ + IPv6: &CloudInitIPv6Config{SLAAC: true}}, + QemuNetworkInterfaceID2: CloudInitNetworkConfig{ + IPv4: &CloudInitIPv4Config{ + Address: util.Pointer(IPv4CIDR("192.168.1.10/24")), + Gateway: util.Pointer(IPv4Address("192.168.56.1"))}, + IPv6: &CloudInitIPv6Config{ + Address: util.Pointer(IPv6CIDR("2001:0db8:abcd::/48")), + Gateway: util.Pointer(IPv6Address("2001:0db8:abcd::1"))}}, + QemuNetworkInterfaceID31: CloudInitNetworkConfig{ + IPv4: &CloudInitIPv4Config{Address: util.Pointer(IPv4CIDR("10.20.4.7/22"))}}}}}}, + {name: `CloudInit PublicSSHkeys`, + input: map[string]interface{}{"sshkeys": test_data_qemu.PublicKey_Encoded_Input()}, + output: &ConfigQemu{CloudInit: &CloudInit{ + NetworkInterfaces: CloudInitNetworkInterfaces{}, + PublicSSHkeys: util.Pointer(test_data_qemu.PublicKey_Decoded_Output())}}}, + {name: `CloudInit UpgradePackages`, + input: map[string]interface{}{"ciupgrade": float64(0)}, + output: &ConfigQemu{CloudInit: &CloudInit{ + NetworkInterfaces: CloudInitNetworkInterfaces{}, + UpgradePackages: util.Pointer(false)}}}, + {name: `CloudInit UserPassword`, + input: map[string]interface{}{"cipassword": string("Enter123!")}, + output: &ConfigQemu{CloudInit: &CloudInit{ + NetworkInterfaces: CloudInitNetworkInterfaces{}, + UserPassword: util.Pointer("Enter123!")}}}, + {name: `CloudInit Username`, + input: map[string]interface{}{"ciuser": string("root")}, + output: &ConfigQemu{CloudInit: &CloudInit{ + NetworkInterfaces: CloudInitNetworkInterfaces{}, + Username: util.Pointer("root")}}}, + {name: `CloudInit Username empty`, + input: map[string]interface{}{"ciuser": string(" ")}, + output: &ConfigQemu{}}, // Disks Ide CdRom {name: "Disks Ide CdRom none", input: map[string]interface{}{"ide1": "none,media=cdrom"}, @@ -6084,6 +6652,54 @@ func Test_ConfigQemu_Validate(t *testing.T) { // Valid Agent {name: "Valid Agent", input: ConfigQemu{Agent: &QemuGuestAgent{Type: util.Pointer(QemuGuestAgentType("isa"))}}}, + // Valid CloudInit + {name: `Valid CloudInit`, + input: ConfigQemu{CloudInit: &CloudInit{Custom: &CloudInitCustom{ + Meta: &CloudInitSnippet{FilePath: CloudInitSnippetPath(test_data_qemu.CloudInitSnippetPath_Max_Legal())}, + Network: &CloudInitSnippet{FilePath: ""}, + User: &CloudInitSnippet{FilePath: CloudInitSnippetPath(test_data_qemu.CloudInitSnippetPath_Max_Legal())}, + Vendor: &CloudInitSnippet{FilePath: ""}}, + NetworkInterfaces: CloudInitNetworkInterfaces{ + QemuNetworkInterfaceID0: CloudInitNetworkConfig{ + IPv4: util.Pointer(CloudInitIPv4Config{Address: util.Pointer(IPv4CIDR("192.168.45.1/24"))})}, + QemuNetworkInterfaceID1: CloudInitNetworkConfig{ + IPv4: util.Pointer(CloudInitIPv4Config{Address: util.Pointer(IPv4CIDR(""))})}, + QemuNetworkInterfaceID2: CloudInitNetworkConfig{ + IPv4: util.Pointer(CloudInitIPv4Config{ + Address: util.Pointer(IPv4CIDR("")), + DHCP: true})}, + QemuNetworkInterfaceID3: CloudInitNetworkConfig{ + IPv4: util.Pointer(CloudInitIPv4Config{ + Gateway: util.Pointer(IPv4Address("")), + DHCP: true})}, + QemuNetworkInterfaceID4: CloudInitNetworkConfig{ + IPv4: util.Pointer(CloudInitIPv4Config{Gateway: util.Pointer(IPv4Address("192.168.45.1"))})}, + QemuNetworkInterfaceID5: CloudInitNetworkConfig{ + IPv4: util.Pointer(CloudInitIPv4Config{Gateway: util.Pointer(IPv4Address(""))})}, + QemuNetworkInterfaceID9: CloudInitNetworkConfig{ + IPv6: util.Pointer(CloudInitIPv6Config{Address: util.Pointer(IPv6CIDR("2001:0db8:85a3::/64"))})}, + QemuNetworkInterfaceID10: CloudInitNetworkConfig{ + IPv6: util.Pointer(CloudInitIPv6Config{Address: util.Pointer(IPv6CIDR(""))})}, + QemuNetworkInterfaceID11: CloudInitNetworkConfig{ + IPv6: util.Pointer(CloudInitIPv6Config{ + Address: util.Pointer(IPv6CIDR("")), + DHCP: true})}, + QemuNetworkInterfaceID12: CloudInitNetworkConfig{ + IPv6: util.Pointer(CloudInitIPv6Config{ + Gateway: util.Pointer(IPv6Address("")), + DHCP: true})}, + QemuNetworkInterfaceID13: CloudInitNetworkConfig{ + IPv6: util.Pointer(CloudInitIPv6Config{Gateway: util.Pointer(IPv6Address("3f6d:5b2a:1e4d:7c91:abcd:1234:5678:9abc"))})}, + QemuNetworkInterfaceID14: CloudInitNetworkConfig{ + IPv6: util.Pointer(CloudInitIPv6Config{Gateway: util.Pointer(IPv6Address(""))})}, + QemuNetworkInterfaceID15: CloudInitNetworkConfig{ + IPv6: util.Pointer(CloudInitIPv6Config{ + Address: util.Pointer(IPv6CIDR("")), + SLAAC: true})}, + QemuNetworkInterfaceID16: CloudInitNetworkConfig{ + IPv6: util.Pointer(CloudInitIPv6Config{ + Gateway: util.Pointer(IPv6Address("")), + SLAAC: true})}}}}}, // Valid Disks {name: "Valid Disks Empty 0", input: ConfigQemu{Disks: &QemuStorages{}}, @@ -6215,6 +6831,84 @@ func Test_ConfigQemu_Validate(t *testing.T) { {name: "Invalid Agent", input: ConfigQemu{Agent: &QemuGuestAgent{Type: util.Pointer(QemuGuestAgentType("test"))}}, err: errors.New(QemuGuestAgentType_Error_Invalid)}, + // Invalid CloudInit + {name: `Invalid CloudInit errors.New(CloudInitSnippetPath_Error_InvalidCharacters)`, + input: ConfigQemu{CloudInit: &CloudInit{Custom: &CloudInitCustom{Meta: &CloudInitSnippet{ + FilePath: CloudInitSnippetPath(test_data_qemu.CloudInitSnippetPath_Character_Illegal()[0])}}}}, + err: errors.New(CloudInitSnippetPath_Error_InvalidCharacters)}, + {name: `Invalid CloudInit errors.New(CloudInitSnippetPath_Error_InvalidPath)`, + input: ConfigQemu{CloudInit: &CloudInit{Custom: &CloudInitCustom{Network: &CloudInitSnippet{ + FilePath: CloudInitSnippetPath(test_data_qemu.CloudInitSnippetPath_InvalidPath())}}}}, + err: errors.New(CloudInitSnippetPath_Error_InvalidPath)}, + {name: `Invalid CloudInit errors.New(CloudInitSnippetPath_Error_MaxLength)`, + input: ConfigQemu{CloudInit: &CloudInit{Custom: &CloudInitCustom{User: &CloudInitSnippet{ + FilePath: CloudInitSnippetPath(test_data_qemu.CloudInitSnippetPath_Max_Illegal())}}}}, + err: errors.New(CloudInitSnippetPath_Error_MaxLength)}, + {name: `Invalid CloudInit errors.New(CloudInitSnippetPath_Error_Relative)`, + input: ConfigQemu{CloudInit: &CloudInit{Custom: &CloudInitCustom{Vendor: &CloudInitSnippet{FilePath: CloudInitSnippetPath(test_data_qemu.CloudInitSnippetPath_Relative())}}}}, + err: errors.New(CloudInitSnippetPath_Error_Relative)}, + {name: `Invalid CloudInit errors.New(QemuNetworkInterfaceID_Error_Invalid)`, + input: ConfigQemu{CloudInit: &CloudInit{NetworkInterfaces: CloudInitNetworkInterfaces{ + 32: CloudInitNetworkConfig{}}}}, + err: errors.New(QemuNetworkInterfaceID_Error_Invalid)}, + {name: `Invalid CloudInit CloudInitNetworkInterfaces IPv4 Address Mutually exclusive with DHCP`, + input: ConfigQemu{CloudInit: &CloudInit{NetworkInterfaces: CloudInitNetworkInterfaces{QemuNetworkInterfaceID5: CloudInitNetworkConfig{ + IPv4: util.Pointer(CloudInitIPv4Config{ + Address: util.Pointer(IPv4CIDR("192.168.45.1/24")), + DHCP: true})}}}}, + err: errors.New(CloudInitIPv4Config_Error_DhcpAddressMutuallyExclusive)}, + {name: `Invalid CloudInit CloudInitNetworkInterfaces IPv4 Gateway Mutually exclusive with DHCP`, + input: ConfigQemu{CloudInit: &CloudInit{NetworkInterfaces: CloudInitNetworkInterfaces{QemuNetworkInterfaceID6: CloudInitNetworkConfig{ + IPv4: util.Pointer(CloudInitIPv4Config{ + Gateway: util.Pointer(IPv4Address("192.168.45.1")), + DHCP: true})}}}}, + err: errors.New(CloudInitIPv4Config_Error_DhcpGatewayMutuallyExclusive)}, + {name: `Invalid CloudInit CloudInitNetworkInterfaces IPv4 Address errors.New(IPv4CIDR_Error_Invalid)`, + input: ConfigQemu{CloudInit: &CloudInit{NetworkInterfaces: CloudInitNetworkInterfaces{QemuNetworkInterfaceID7: CloudInitNetworkConfig{ + IPv4: util.Pointer(CloudInitIPv4Config{Address: util.Pointer(IPv4CIDR("192.168.45.1"))})}}}}, + err: errors.New(IPv4CIDR_Error_Invalid)}, + {name: `Invalid CloudInit CloudInitNetworkInterfaces IPv4 Gateway errors.New(IPv4Address_Error_Invalid)`, + input: ConfigQemu{CloudInit: &CloudInit{NetworkInterfaces: CloudInitNetworkInterfaces{QemuNetworkInterfaceID8: CloudInitNetworkConfig{ + IPv4: util.Pointer(CloudInitIPv4Config{Gateway: util.Pointer(IPv4Address("192.168.45.1/24"))})}}}}, + err: errors.New(IPv4Address_Error_Invalid)}, + {name: `Invalid CloudInit CloudInitNetworkInterfaces IPv6 Address Mutually exclusive with DHCP`, + input: ConfigQemu{CloudInit: &CloudInit{NetworkInterfaces: CloudInitNetworkInterfaces{QemuNetworkInterfaceID17: CloudInitNetworkConfig{ + IPv6: util.Pointer(CloudInitIPv6Config{ + Address: util.Pointer(IPv6CIDR("2001:0db8:85a3::/64")), + DHCP: true})}}}}, + err: errors.New(CloudInitIPv6Config_Error_DhcpAddressMutuallyExclusive)}, + {name: `Invalid CloudInit CloudInitNetworkInterfaces IPv6 Address Mutually exclusive with SLAAC`, + input: ConfigQemu{CloudInit: &CloudInit{NetworkInterfaces: CloudInitNetworkInterfaces{QemuNetworkInterfaceID18: CloudInitNetworkConfig{ + IPv6: util.Pointer(CloudInitIPv6Config{ + Address: util.Pointer(IPv6CIDR("2001:0db8:85a3::/64")), + SLAAC: true})}}}}, + err: errors.New(CloudInitIPv6Config_Error_SlaacAddressMutuallyExclusive)}, + {name: `Invalid CloudInit CloudInitNetworkInterfaces IPv6 DHCP Mutually exclusive with SLAAC`, + input: ConfigQemu{CloudInit: &CloudInit{NetworkInterfaces: CloudInitNetworkInterfaces{QemuNetworkInterfaceID19: CloudInitNetworkConfig{ + IPv6: util.Pointer(CloudInitIPv6Config{ + DHCP: true, + SLAAC: true})}}}}, + err: errors.New(CloudInitIPv6Config_Error_DhcpSlaacMutuallyExclusive)}, + {name: `Invalid CloudInit CloudInitNetworkInterfaces IPv6 Gateway Mutually exclusive with DHCP`, + input: ConfigQemu{CloudInit: &CloudInit{NetworkInterfaces: CloudInitNetworkInterfaces{QemuNetworkInterfaceID20: CloudInitNetworkConfig{ + IPv6: util.Pointer(CloudInitIPv6Config{ + Gateway: util.Pointer(IPv6Address("3f6d:5b2a:1e4d:7c91:abcd:1234:5678:9abc")), + DHCP: true})}}}}, + err: errors.New(CloudInitIPv6Config_Error_DhcpGatewayMutuallyExclusive)}, + {name: `Invalid CloudInit CloudInitNetworkInterfaces IPv6 Gateway Mutually exclusive with SLAAC`, + input: ConfigQemu{CloudInit: &CloudInit{NetworkInterfaces: CloudInitNetworkInterfaces{QemuNetworkInterfaceID21: CloudInitNetworkConfig{ + IPv6: util.Pointer(CloudInitIPv6Config{ + Gateway: util.Pointer(IPv6Address("3f6d:5b2a:1e4d:7c91:abcd:1234:5678:9abc")), + SLAAC: true})}}}}, + err: errors.New(CloudInitIPv6Config_Error_SlaacGatewayMutuallyExclusive)}, + {name: `Invalid CloudInit CloudInitNetworkInterfaces IPv6 Address errors.New(IPv6CIDR_Error_Invalid)`, + input: ConfigQemu{CloudInit: &CloudInit{NetworkInterfaces: CloudInitNetworkInterfaces{QemuNetworkInterfaceID22: CloudInitNetworkConfig{ + IPv6: util.Pointer(CloudInitIPv6Config{Address: util.Pointer(IPv6CIDR("3f6d:5b2a:1e4d:7c91:abcd:1234:5678:9abc"))})}}}}, + err: errors.New(IPv6CIDR_Error_Invalid)}, + {name: `Invalid CloudInit CloudInitNetworkInterfaces IPv6 Gateway errors.New(IPv6Address_Error_Invalid)`, + input: ConfigQemu{CloudInit: &CloudInit{NetworkInterfaces: CloudInitNetworkInterfaces{QemuNetworkInterfaceID23: CloudInitNetworkConfig{ + IPv6: util.Pointer(CloudInitIPv6Config{Gateway: util.Pointer(IPv6Address("2001:0db8:85a3::/64"))})}}}}, + err: errors.New(IPv6Address_Error_Invalid)}, // Invalid Disks Mutually exclusive Ide {name: "Invalid Disks MutuallyExclusive Ide 0", input: ConfigQemu{Disks: &QemuStorages{Ide: &QemuIdeDisks{Disk_0: &QemuIdeStorage{ @@ -7341,28 +8035,21 @@ func Test_ConfigQemu_Validate(t *testing.T) { } } -// Test the encoding logic to encode the ssh keys -func Test_sshKeyUrlEncode(t *testing.T) { - input := test_sshKeyUrlEncode_Input() - output := test_sshKeyUrlEncode_Output() - test := sshKeyUrlEncode(input) - require.Equal(t, output, test) -} - -func test_sshKeyUrlEncode_Input() string { - keys := []string{ - "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDT7lsC9gTAjL0FUPlHqnz71TzqDMdsdHhWu54M7NN4E9KNzKwzUy1h6ZuOMm+d0nWX+yuT2Mfzi8NaKe5ATg0bwmrzZ1ikS/tGs7v/TyMSBOlmrS5v0g8rn40bphCqnNeNcfP9JR2zyq4UccpdIYA62t6Ky9d/WBbsAQRESwZVhpU9JGhwnVHFcNN5svlDwz9wzW1a2J2/E76+vym+3Rt4W9s3MqQZdbHozo4N43puXq7PH1tTr/RT84uaMF4XLx1CUm+bMZLgtac8sHl1DJz4gC3MLasD6UXZzRz99K+QAHD6YsXHDwdWu6QAkqzS0DNDbm0E618wn4GEZAJJhehh test@VScode", - "ssh-dss AAAAB3NzaC1kc3MAAACBAN6VwM2CMPrpz0CT8z4UP5we4Jt1MSDHumArdzTaxaqtAcV6Z+a4ZO/0geqEDZJSideX7Iq8zYrzdXGXfR+8N5GHoz49mVFit101cKAvcwZhzVeXQ1Cc8Zyjk53qmjWiNonfsjxP9VorNjjb/zGnA3ZnazflfyzqwEr8fV7JtUwjAAAAFQDlk3FT+QmsKiiBjBuekwyFeVzwiwAAAIBeAlzP9hsVeEbPjEjkxi9/hVgNQE8xtuUMZUCq7NOu5RlGzPHStzh8ByMh0Jsly0GbVHUfM84ikSpU/L5O3j75vq+cng77mezAGWfHfBpAL+whKfXvYHy0mqb0M1krzbdRbQkt9TV4gNw+Nac17jmfnRBebYYoJltehCognAU+xAAAAIEAmI1SEcjqSTHRnHeypg08ppcpRUGx0Mkcb/Moos2SVfSfWBXrNR7p6eRzVPN0gCXSLsiaE0DaRvM+GPRJeffCh4+Ahx84Gptf0m+EXH47sPfsumk8XxItDZa4zYYJ2gAISBdLD06iMtmJWAzD59FXDaHedxom9/Hb7oQXHEUzQQY= test@VScode", - "ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAF9dgZNa82njYtBR2zhCQs1yHL/GqA+AAmz97bjj2t2EQwMepx3TT8RubZscqwt6yedPREJU/8x0XtoEWkQzjBkGgCc2ip8xGyy6j3Th9YtYj9gW1g7Rwmqwnz0ZOd/l12tC3q7ujS7hlInkhxbOyhqNXZ+obseOaS0g5Toqpgr+mV1Rg== test@VScode", - "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEY5T2JQgiL5Z5Yuy4yXuUYglVJlpsokHFXR1hvnCVYW cardno:18 228 342"} - return strings.Join(keys, "\n") -} - -func test_sshKeyUrlEncode_Output() string { - encodedKeys := []string{ - "ssh-rsa%20AAAAB3NzaC1yc2EAAAADAQABAAABAQDT7lsC9gTAjL0FUPlHqnz71TzqDMdsdHhWu54M7NN4E9KNzKwzUy1h6ZuOMm%2Bd0nWX%2ByuT2Mfzi8NaKe5ATg0bwmrzZ1ikS%2FtGs7v%2FTyMSBOlmrS5v0g8rn40bphCqnNeNcfP9JR2zyq4UccpdIYA62t6Ky9d%2FWBbsAQRESwZVhpU9JGhwnVHFcNN5svlDwz9wzW1a2J2%2FE76%2Bvym%2B3Rt4W9s3MqQZdbHozo4N43puXq7PH1tTr%2FRT84uaMF4XLx1CUm%2BbMZLgtac8sHl1DJz4gC3MLasD6UXZzRz99K%2BQAHD6YsXHDwdWu6QAkqzS0DNDbm0E618wn4GEZAJJhehh%20test%40VScode", - "ssh-dss%20AAAAB3NzaC1kc3MAAACBAN6VwM2CMPrpz0CT8z4UP5we4Jt1MSDHumArdzTaxaqtAcV6Z%2Ba4ZO%2F0geqEDZJSideX7Iq8zYrzdXGXfR%2B8N5GHoz49mVFit101cKAvcwZhzVeXQ1Cc8Zyjk53qmjWiNonfsjxP9VorNjjb%2FzGnA3ZnazflfyzqwEr8fV7JtUwjAAAAFQDlk3FT%2BQmsKiiBjBuekwyFeVzwiwAAAIBeAlzP9hsVeEbPjEjkxi9%2FhVgNQE8xtuUMZUCq7NOu5RlGzPHStzh8ByMh0Jsly0GbVHUfM84ikSpU%2FL5O3j75vq%2Bcng77mezAGWfHfBpAL%2BwhKfXvYHy0mqb0M1krzbdRbQkt9TV4gNw%2BNac17jmfnRBebYYoJltehCognAU%2BxAAAAIEAmI1SEcjqSTHRnHeypg08ppcpRUGx0Mkcb%2FMoos2SVfSfWBXrNR7p6eRzVPN0gCXSLsiaE0DaRvM%2BGPRJeffCh4%2BAhx84Gptf0m%2BEXH47sPfsumk8XxItDZa4zYYJ2gAISBdLD06iMtmJWAzD59FXDaHedxom9%2FHb7oQXHEUzQQY%3D%20test%40VScode", - "ecdsa-sha2-nistp521%20AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAF9dgZNa82njYtBR2zhCQs1yHL%2FGqA%2BAAmz97bjj2t2EQwMepx3TT8RubZscqwt6yedPREJU%2F8x0XtoEWkQzjBkGgCc2ip8xGyy6j3Th9YtYj9gW1g7Rwmqwnz0ZOd%2Fl12tC3q7ujS7hlInkhxbOyhqNXZ%2BobseOaS0g5Toqpgr%2BmV1Rg%3D%3D%20test%40VScode", - "ssh-ed25519%20AAAAC3NzaC1lZDI1NTE5AAAAIEY5T2JQgiL5Z5Yuy4yXuUYglVJlpsokHFXR1hvnCVYW%20cardno%3A18%20228%20342"} - return strings.Join(encodedKeys, "%0A") + "%0A" +func Test_QemuNetworkInterfaceID_Validate(t *testing.T) { + tests := []struct { + name string + input QemuNetworkInterfaceID + output error + }{ + {name: "Valid", + input: QemuNetworkInterfaceID0}, + {name: "Invalid", + input: 32, + output: errors.New(QemuNetworkInterfaceID_Error_Invalid)}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require.Equal(t, test.output, test.input.Validate()) + }) + } } diff --git a/proxmox/util.go b/proxmox/util.go index d5925ccc..b6b268d5 100644 --- a/proxmox/util.go +++ b/proxmox/util.go @@ -236,6 +236,14 @@ func floatToTrimmedString(f float64, maxDecimals uint8) (s string) { return } +func isIPv4(address string) bool { + return strings.Count(address, ":") == 0 +} + +func isIPv6(address string) bool { + return strings.Count(address, ":") > 2 +} + func splitStringOfSettings(settings string) map[string]interface{} { settingValuePairs := strings.Split(settings, ",") settingMap := map[string]interface{}{} @@ -263,3 +271,9 @@ func subtractArray[T comparable](A, B []T) (result []T) { } return } + +// To be used during testing +func uninitializedArray[T any]() []T { + var x []T + return x +} diff --git a/test/api/CloudInit/cloudinit_test.go b/test/api/CloudInit/cloudinit_test.go index 7a117e3f..57ec7d1e 100644 --- a/test/api/CloudInit/cloudinit_test.go +++ b/test/api/CloudInit/cloudinit_test.go @@ -3,6 +3,7 @@ package api_test import ( "testing" + "github.com/Telmate/proxmox-api-go/internal/util" pxapi "github.com/Telmate/proxmox-api-go/proxmox" api_test "github.com/Telmate/proxmox-api-go/test/api" "github.com/stretchr/testify/require" @@ -33,17 +34,24 @@ func Test_Cloud_Init_VM(t *testing.T) { err = config.Create(vmref, Test.GetClient()) require.NoError(t, err) - config.Ipconfig = pxapi.IpconfigMap{} config.Boot = "order=virtio0;ide2;net0" - config.Ipconfig[0] = "gw=10.0.0.1,ip=10.0.0.2/24" - + config.CloudInit = &pxapi.CloudInit{ + NetworkInterfaces: pxapi.CloudInitNetworkInterfaces{ + pxapi.QemuNetworkInterfaceID0: pxapi.CloudInitNetworkConfig{ + IPv4: &pxapi.CloudInitIPv4Config{ + Address: util.Pointer(pxapi.IPv4CIDR("10.0.0.2/24")), + Gateway: util.Pointer(pxapi.IPv4Address("10.0.0.1"))}}}} _, err = config.Update(true, vmref, Test.GetClient()) require.NoError(t, err) testConfig, _ := pxapi.NewConfigQemuFromApi(vmref, Test.GetClient()) - require.Equal(t, testConfig.Ipconfig[0], "gw=10.0.0.1,ip=10.0.0.2/24") + require.Equal(t, testConfig.CloudInit.NetworkInterfaces[pxapi.QemuNetworkInterfaceID0], + pxapi.CloudInitNetworkConfig{ + IPv4: &pxapi.CloudInitIPv4Config{ + Address: util.Pointer(pxapi.IPv4CIDR("10.0.0.2/24")), + Gateway: util.Pointer(pxapi.IPv4Address("10.0.0.1"))}}) _, err = Test.GetClient().DeleteVm(vmref) require.NoError(t, err) diff --git a/test/data/test_data_qemu/type_CloudInitSnippetPath.go b/test/data/test_data_qemu/type_CloudInitSnippetPath.go new file mode 100644 index 00000000..867535ee --- /dev/null +++ b/test/data/test_data_qemu/type_CloudInitSnippetPath.go @@ -0,0 +1,123 @@ +package test_data_qemu + +// illegal character +func CloudInitSnippetPath_Character_Illegal() []string { + return []string{ + "aBc123!4567890_-", + "Qwer@ty-1234_ABCDEFGHIJKLMNOPQRSTUVWXYZ", + "x1y2#z3_4-5-6-7-8-9", + "HelloWo$rld_2023", + "Ab1_%cd2_ef3-gh4-ij5", + "a-_-^_-_-_-_-_-_-_-_-_-", + "snaps&hotName_2433242", + "A1_B2-*C3_D4-E5_F6", + "Xyz-123(_456_789-0", + "Test_Cas)e-123_456_789_0", + "a_1+", + "B-c_=2-D", + "E3_f4-G5_:H6-I7", + "JKL_MNO_PQ;R-STU_VWX_YZ0", + "aBgnhfjkfgd'ihfghudsfgio", + `Cdsdjfidshfu"isdghfsgffghdsufsdhfgdsfuah`, + "Ef-`gh", + "Ij-k~l-mn", + "Op-qr-st-u-vw-xy-z0-12-34-56-[78-90", + "Abcd_1234-EFGH_]5678-IJKL_9012", + "M-n-Op-qR-sT-uV{-wX-yZ", + "a_b-c_d_e-f_g_h_}i_j_k_l_m_n-o-p-q-r-s-t", + "Aa1_Bb2-C,c3_Dd4-Ee5_Ff6-Gg7_Hh8-Ii9", + "JjKkLl-MmNnOo.PpQ)qRrSsTtUuVvWwXxYyZz01", + "A->1", + "B-2<_C-3", + "D-4_?E-5-F-6", + "G-7-H/-8*-I-9", + `J-0_K-\1-L-2-M-3-N-4-O-5-P-6-Q-7-R-8-S-9`, + "T-0_U-1-|V-2-W-3-X-4-Y-5-Z-6-7-8-9-0", + "a2😀", + } +} + +// 256 valid characters +func CloudInitSnippetPath_Max_Legal() string { + return "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789/- _.abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789/- _.abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789/- _.abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ012" +} + +// 257 invalid characters +func CloudInitSnippetPath_Max_Illegal() string { + return CloudInitSnippetPath_Max_Legal() + "A" +} + +// 1 valid characters +func CloudInitSnippetPath_Min_Legal() string { + return CloudInitSnippetPath_Min_Illegal() + "a" +} + +// 2 invalid characters +func CloudInitSnippetPath_Min_Illegal() string { + return "" +} + +func CloudInitSnippetPath_Legal() []string { + return []string{ + "aBc1234567890_-", + "Qwerty-1234_ABCDEFGHIJKLMNOPQRSTUVWXYZ", + "x1y2z3_4-5-6-7-8-9", + "HelloWorld_2023", + "Ab1_cd2_ef3-gh4-ij5", + "a-_-_-_-_-_-_-_-_-_-_-", + "snapshotName_2433242", + "A1_B2-C3_D4-E5_F6", + "Xyz-123_456_789-0", + "Test_Case-123_456_789_0", + "a_1", + "B-c_2-D", + "E3_f4-G5_H6-I7", + "JKL_MNO_PQR-STU_VWX_YZ0", + "aBgnhfjkfgdihfghudsfgio", + "Cdsdjfidshfuisdghfsgffghdsufsdhfgdsfuahs", + "Ef-gh", + "Ij-kl-mn", + "Op-qr-st-u-vw-xy-z0-12-34-56-78-90", + "Abcd_1234-EFGH_5678-IJKL_9012", + "M-n-Op-qR-sT-uV-wX-yZ", + "a_b-c_d_e-f_g_h_i_j_k_l_m_n-o-p-q-r-s-t-", + "Aa1_Bb2-Cc3_Dd4-Ee5_Ff6-Gg7_Hh8-Ii9", + "JjKkLl-MmNnOoPpQqRrSsTtUuVvWwXxYyZz01", + "A-1", + "B-2_C-3", + "D-4_E-5-F-6", + "G-7-H-8-I-9", + "J-0_K-1-L-2-M-3-N-4-O-5-P-6-Q-7-R-8-S-9", + "T-0_U-1-V-2-W-3-X-4-Y-5-Z-6-7-8-9-0", + "a2B", + "c4D", + "e6F-g8H-i0J", + "k2L-m4N-o6P-q8R-s0T", + "u2V-w4X-y6Z-01-23-45-67-89-0", + "Abc_1234-Def_5678-Ghi_9012-Jkl_3456-Mno_", + "Pqr_2345-Stu_6789-Vwx_0123-Yz0_4567", + "a-B", + "c-D_e-F", + "g-H_i-J-k-L", + "m-N-o-P_q-R-s-T-u-V-w-X-y-Z-0", + "A_1b2-C3d4_E5f6-G7h8_I9j0-K1l2-M3n4", + "O5p6-Q7r8-S9t0-U1v2-W3x4-Y5z6-01", + "A2b3-C4d5-E6f7-G8h9-I0j1-K2l3-M4n5-O6", + "P7q8-R9s0-T1u2-V3w4-X5y6-Z7-89-01-23-45-", + "Ab_12-cD_34-eF_56-gH_78-iJ_90-kL_12-mN_3", + "O5p6-Q7r8-S9t0-U1v2-W3x4-Y5z6-01-23-45", + "A7b8-C9d0-E1f2-G3h4-I5j6-K7l8-M9n0-O1p2-", + "S5t6-U7v8-W9x0-Y1z2-34-56-78-90-12-34-56", + "Ab1C_d2E-F3G_h4I-J5k6L-m7N-o8P-q9R-s0T-u", + CloudInitSnippetPath_Max_Legal(), + CloudInitSnippetPath_Min_Legal(), + } +} + +func CloudInitSnippetPath_InvalidPath() string { + return "yhfg8fiusfhis/fmdjfhsudf//dsad" +} + +func CloudInitSnippetPath_Relative() string { + return "/file" +} diff --git a/test/data/test_data_qemu/type_PublicKey.go b/test/data/test_data_qemu/type_PublicKey.go new file mode 100644 index 00000000..ca8abe70 --- /dev/null +++ b/test/data/test_data_qemu/type_PublicKey.go @@ -0,0 +1,42 @@ +package test_data_qemu + +import ( + "crypto" + "strings" +) + +func PublicKey_Decoded_Output() []crypto.PublicKey { + return []crypto.PublicKey{ + "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDT7lsC9gTAjL0FUPlHqnz71TzqDMdsdHhWu54M7NN4E9KNzKwzUy1h6ZuOMm+d0nWX+yuT2Mfzi8NaKe5ATg0bwmrzZ1ikS/tGs7v/TyMSBOlmrS5v0g8rn40bphCqnNeNcfP9JR2zyq4UccpdIYA62t6Ky9d/WBbsAQRESwZVhpU9JGhwnVHFcNN5svlDwz9wzW1a2J2/E76+vym+3Rt4W9s3MqQZdbHozo4N43puXq7PH1tTr/RT84uaMF4XLx1CUm+bMZLgtac8sHl1DJz4gC3MLasD6UXZzRz99K+QAHD6YsXHDwdWu6QAkqzS0DNDbm0E618wn4GEZAJJhehh test@VScode", + "ssh-dss AAAAB3NzaC1kc3MAAACBAN6VwM2CMPrpz0CT8z4UP5we4Jt1MSDHumArdzTaxaqtAcV6Z+a4ZO/0geqEDZJSideX7Iq8zYrzdXGXfR+8N5GHoz49mVFit101cKAvcwZhzVeXQ1Cc8Zyjk53qmjWiNonfsjxP9VorNjjb/zGnA3ZnazflfyzqwEr8fV7JtUwjAAAAFQDlk3FT+QmsKiiBjBuekwyFeVzwiwAAAIBeAlzP9hsVeEbPjEjkxi9/hVgNQE8xtuUMZUCq7NOu5RlGzPHStzh8ByMh0Jsly0GbVHUfM84ikSpU/L5O3j75vq+cng77mezAGWfHfBpAL+whKfXvYHy0mqb0M1krzbdRbQkt9TV4gNw+Nac17jmfnRBebYYoJltehCognAU+xAAAAIEAmI1SEcjqSTHRnHeypg08ppcpRUGx0Mkcb/Moos2SVfSfWBXrNR7p6eRzVPN0gCXSLsiaE0DaRvM+GPRJeffCh4+Ahx84Gptf0m+EXH47sPfsumk8XxItDZa4zYYJ2gAISBdLD06iMtmJWAzD59FXDaHedxom9/Hb7oQXHEUzQQY= test@VScode", + "ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAF9dgZNa82njYtBR2zhCQs1yHL/GqA+AAmz97bjj2t2EQwMepx3TT8RubZscqwt6yedPREJU/8x0XtoEWkQzjBkGgCc2ip8xGyy6j3Th9YtYj9gW1g7Rwmqwnz0ZOd/l12tC3q7ujS7hlInkhxbOyhqNXZ+obseOaS0g5Toqpgr+mV1Rg== test@VScode", + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEY5T2JQgiL5Z5Yuy4yXuUYglVJlpsokHFXR1hvnCVYW cardno:18 228 342"} +} + +func PublicKey_Decoded_Input() []crypto.PublicKey { + return []crypto.PublicKey{ + "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDT7lsC9gTAjL0FUPlHqnz71TzqDMdsdHhWu54M7NN4E9KNzKwzUy1h6ZuOMm+d0nWX+yuT2Mfzi8NaKe5ATg0bwmrzZ1ikS/tGs7v/TyMSBOlmrS5v0g8rn40bphCqnNeNcfP9JR2zyq4UccpdIYA62t6Ky9d/WBbsAQRESwZVhpU9JGhwnVHFcNN5svlDwz9wzW1a2J2/E76+vym+3Rt4W9s3MqQZdbHozo4N43puXq7PH1tTr/RT84uaMF4XLx1CUm+bMZLgtac8sHl1DJz4gC3MLasD6UXZzRz99K+QAHD6YsXHDwdWu6QAkqzS0DNDbm0E618wn4GEZAJJhehh test@VScode", + "ssh-dss AAAAB3NzaC1kc3MAAACBAN6VwM2CMPrpz0CT8z4UP5we4Jt1MSDHumArdzTaxaqtAcV6Z+a4ZO/0geqEDZJSideX7Iq8zYrzdXGXfR+8N5GHoz49mVFit101cKAvcwZhzVeXQ1Cc8Zyjk53qmjWiNonfsjxP9VorNjjb/zGnA3ZnazflfyzqwEr8fV7JtUwjAAAAFQDlk3FT+QmsKiiBjBuekwyFeVzwiwAAAIBeAlzP9hsVeEbPjEjkxi9/hVgNQE8xtuUMZUCq7NOu5RlGzPHStzh8ByMh0Jsly0GbVHUfM84ikSpU/L5O3j75vq+cng77mezAGWfHfBpAL+whKfXvYHy0mqb0M1krzbdRbQkt9TV4gNw+Nac17jmfnRBebYYoJltehCognAU+xAAAAIEAmI1SEcjqSTHRnHeypg08ppcpRUGx0Mkcb/Moos2SVfSfWBXrNR7p6eRzVPN0gCXSLsiaE0DaRvM+GPRJeffCh4+Ahx84Gptf0m+EXH47sPfsumk8XxItDZa4zYYJ2gAISBdLD06iMtmJWAzD59FXDaHedxom9/Hb7oQXHEUzQQY= test@VScode", + "ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAF9dgZNa82njYtBR2zhCQs1yHL/GqA+AAmz97bjj2t2EQwMepx3TT8RubZscqwt6yedPREJU/8x0XtoEWkQzjBkGgCc2ip8xGyy6j3Th9YtYj9gW1g7Rwmqwnz0ZOd/l12tC3q7ujS7hlInkhxbOyhqNXZ+obseOaS0g5Toqpgr+mV1Rg== test@VScode", + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEY5T2JQgiL5Z5Yuy4yXuUYglVJlpsokHFXR1hvnCVYW cardno:18 228 342"} +} + +func PublicKey_Encoded_Output() string { + return strings.Join([]string{ + "ssh-rsa%20AAAAB3NzaC1yc2EAAAADAQABAAABAQDT7lsC9gTAjL0FUPlHqnz71TzqDMdsdHhWu54M7NN4E9KNzKwzUy1h6ZuOMm%2Bd0nWX%2ByuT2Mfzi8NaKe5ATg0bwmrzZ1ikS%2FtGs7v%2FTyMSBOlmrS5v0g8rn40bphCqnNeNcfP9JR2zyq4UccpdIYA62t6Ky9d%2FWBbsAQRESwZVhpU9JGhwnVHFcNN5svlDwz9wzW1a2J2%2FE76%2Bvym%2B3Rt4W9s3MqQZdbHozo4N43puXq7PH1tTr%2FRT84uaMF4XLx1CUm%2BbMZLgtac8sHl1DJz4gC3MLasD6UXZzRz99K%2BQAHD6YsXHDwdWu6QAkqzS0DNDbm0E618wn4GEZAJJhehh%20test%40VScode", + "ssh-dss%20AAAAB3NzaC1kc3MAAACBAN6VwM2CMPrpz0CT8z4UP5we4Jt1MSDHumArdzTaxaqtAcV6Z%2Ba4ZO%2F0geqEDZJSideX7Iq8zYrzdXGXfR%2B8N5GHoz49mVFit101cKAvcwZhzVeXQ1Cc8Zyjk53qmjWiNonfsjxP9VorNjjb%2FzGnA3ZnazflfyzqwEr8fV7JtUwjAAAAFQDlk3FT%2BQmsKiiBjBuekwyFeVzwiwAAAIBeAlzP9hsVeEbPjEjkxi9%2FhVgNQE8xtuUMZUCq7NOu5RlGzPHStzh8ByMh0Jsly0GbVHUfM84ikSpU%2FL5O3j75vq%2Bcng77mezAGWfHfBpAL%2BwhKfXvYHy0mqb0M1krzbdRbQkt9TV4gNw%2BNac17jmfnRBebYYoJltehCognAU%2BxAAAAIEAmI1SEcjqSTHRnHeypg08ppcpRUGx0Mkcb%2FMoos2SVfSfWBXrNR7p6eRzVPN0gCXSLsiaE0DaRvM%2BGPRJeffCh4%2BAhx84Gptf0m%2BEXH47sPfsumk8XxItDZa4zYYJ2gAISBdLD06iMtmJWAzD59FXDaHedxom9%2FHb7oQXHEUzQQY%3D%20test%40VScode", + "ecdsa-sha2-nistp521%20AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAF9dgZNa82njYtBR2zhCQs1yHL%2FGqA%2BAAmz97bjj2t2EQwMepx3TT8RubZscqwt6yedPREJU%2F8x0XtoEWkQzjBkGgCc2ip8xGyy6j3Th9YtYj9gW1g7Rwmqwnz0ZOd%2Fl12tC3q7ujS7hlInkhxbOyhqNXZ%2BobseOaS0g5Toqpgr%2BmV1Rg%3D%3D%20test%40VScode", + "ssh-ed25519%20AAAAC3NzaC1lZDI1NTE5AAAAIEY5T2JQgiL5Z5Yuy4yXuUYglVJlpsokHFXR1hvnCVYW%20cardno%3A18%20228%20342"}, + "%0A") + "%0A" +} + +func PublicKey_Encoded_Input() string { + return strings.Join([]string{ + "ssh-rsa%20AAAAB3NzaC1yc2EAAAADAQABAAABAQDT7lsC9gTAjL0FUPlHqnz71TzqDMdsdHhWu54M7NN4E9KNzKwzUy1h6ZuOMm%2Bd0nWX%2ByuT2Mfzi8NaKe5ATg0bwmrzZ1ikS%2FtGs7v%2FTyMSBOlmrS5v0g8rn40bphCqnNeNcfP9JR2zyq4UccpdIYA62t6Ky9d%2FWBbsAQRESwZVhpU9JGhwnVHFcNN5svlDwz9wzW1a2J2%2FE76%2Bvym%2B3Rt4W9s3MqQZdbHozo4N43puXq7PH1tTr%2FRT84uaMF4XLx1CUm%2BbMZLgtac8sHl1DJz4gC3MLasD6UXZzRz99K%2BQAHD6YsXHDwdWu6QAkqzS0DNDbm0E618wn4GEZAJJhehh%20test%40VScode", + "ssh-dss%20%20%20%20AAAAB3NzaC1kc3MAAACBAN6VwM2CMPrpz0CT8z4UP5we4Jt1MSDHumArdzTaxaqtAcV6Z%2Ba4ZO%2F0geqEDZJSideX7Iq8zYrzdXGXfR%2B8N5GHoz49mVFit101cKAvcwZhzVeXQ1Cc8Zyjk53qmjWiNonfsjxP9VorNjjb%2FzGnA3ZnazflfyzqwEr8fV7JtUwjAAAAFQDlk3FT%2BQmsKiiBjBuekwyFeVzwiwAAAIBeAlzP9hsVeEbPjEjkxi9%2FhVgNQE8xtuUMZUCq7NOu5RlGzPHStzh8ByMh0Jsly0GbVHUfM84ikSpU%2FL5O3j75vq%2Bcng77mezAGWfHfBpAL%2BwhKfXvYHy0mqb0M1krzbdRbQkt9TV4gNw%2BNac17jmfnRBebYYoJltehCognAU%2BxAAAAIEAmI1SEcjqSTHRnHeypg08ppcpRUGx0Mkcb%2FMoos2SVfSfWBXrNR7p6eRzVPN0gCXSLsiaE0DaRvM%2BGPRJeffCh4%2BAhx84Gptf0m%2BEXH47sPfsumk8XxItDZa4zYYJ2gAISBdLD06iMtmJWAzD59FXDaHedxom9%2FHb7oQXHEUzQQY%3D%20test%40VScode", + "ecdsa-sha2-nistp521%20AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAF9dgZNa82njYtBR2zhCQs1yHL%2FGqA%2BAAmz97bjj2t2EQwMepx3TT8RubZscqwt6yedPREJU%2F8x0XtoEWkQzjBkGgCc2ip8xGyy6j3Th9YtYj9gW1g7Rwmqwnz0ZOd%2Fl12tC3q7ujS7hlInkhxbOyhqNXZ%2BobseOaS0g5Toqpgr%2BmV1Rg%3D%3D%20%20%20%20test%40VScode", + "%0A", + "%0A", + "ssh-ed25519%20AAAAC3NzaC1lZDI1NTE5AAAAIEY5T2JQgiL5Z5Yuy4yXuUYglVJlpsokHFXR1hvnCVYW%20cardno%3A18%20228%20342"}, + "%0A") + "%0A" +}