diff --git a/.gitignore b/.gitignore index fdfd57f4..b8715cc2 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,6 @@ proxmox-api-go .vagrant/ .vscode .env + +coverage.html +coverage.out \ No newline at end of file diff --git a/proxmox/config_qemu.go b/proxmox/config_qemu.go index 12b2a563..2be22613 100644 --- a/proxmox/config_qemu.go +++ b/proxmox/config_qemu.go @@ -6,8 +6,6 @@ import ( "errors" "fmt" "log" - "math/rand" - "net" "regexp" "strconv" "strings" @@ -30,50 +28,50 @@ type ( // ConfigQemu - Proxmox API QEMU options type ConfigQemu struct { - Agent *QemuGuestAgent `json:"agent,omitempty"` - Args string `json:"args,omitempty"` - 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 - CPU *QemuCPU `json:"cpu,omitempty"` - 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 - FullClone *int `json:"fullclone,omitempty"` // TODO should probably be a bool - 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 - 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 *QemuMemory `json:"memory,omitempty"` - 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"` - QemuDisks QemuDevices `json:"disk,omitempty"` // DEPRECATED use Disks *QemuStorages instead - QemuIso string `json:"qemuiso,omitempty"` // DEPRECATED use Iso *IsoFile instead - QemuKVM *bool `json:"kvm,omitempty"` - QemuNetworks QemuDevices `json:"network,omitempty"` // TODO should be a struct - QemuOs string `json:"ostype,omitempty"` - QemuPCIDevices QemuDevices `json:"hostpci,omitempty"` // TODO should be a struct - QemuPxe bool `json:"pxe,omitempty"` - QemuUnusedDisks QemuDevices `json:"unused,omitempty"` // TODO should be a struct - QemuUsbs QemuDevices `json:"usb,omitempty"` // TODO should be a struct - 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 - Serials SerialInterfaces `json:"serials,omitempty"` - Smbios1 string `json:"smbios1,omitempty"` // TODO should be custom type with enum? - Startup string `json:"startup,omitempty"` // TODO should be a struct? - Storage string `json:"storage,omitempty"` // this value is only used when doing a full clone and is never returned - TPM *TpmState `json:"tpm,omitempty"` - Tablet *bool `json:"tablet,omitempty"` - Tags *[]Tag `json:"tags,omitempty"` - VmID int `json:"vmid,omitempty"` // TODO should be a custom type as there are limitations + Agent *QemuGuestAgent `json:"agent,omitempty"` + Args string `json:"args,omitempty"` + 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 + CPU *QemuCPU `json:"cpu,omitempty"` + 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 + FullClone *int `json:"fullclone,omitempty"` // TODO should probably be a bool + 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 + 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 *QemuMemory `json:"memory,omitempty"` + Name string `json:"name,omitempty"` // TODO should be custom type as there are character and length limitations + Networks QemuNetworkInterfaces `json:"networks,omitempty"` + 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"` + QemuDisks QemuDevices `json:"disk,omitempty"` // DEPRECATED use Disks *QemuStorages instead + QemuIso string `json:"qemuiso,omitempty"` // DEPRECATED use Iso *IsoFile instead + QemuKVM *bool `json:"kvm,omitempty"` + QemuOs string `json:"ostype,omitempty"` + QemuPCIDevices QemuDevices `json:"hostpci,omitempty"` // TODO should be a struct + QemuPxe bool `json:"pxe,omitempty"` + QemuUnusedDisks QemuDevices `json:"unused,omitempty"` // TODO should be a struct + QemuUsbs QemuDevices `json:"usb,omitempty"` // TODO should be a struct + 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 + Serials SerialInterfaces `json:"serials,omitempty"` + Smbios1 string `json:"smbios1,omitempty"` // TODO should be custom type with enum? + Startup string `json:"startup,omitempty"` // TODO should be a struct? + Storage string `json:"storage,omitempty"` // this value is only used when doing a full clone and is never returned + TPM *TpmState `json:"tpm,omitempty"` + Tablet *bool `json:"tablet,omitempty"` + Tags *[]Tag `json:"tags,omitempty"` + VmID int `json:"vmid,omitempty"` // TODO should be a custom type as there are limitations } const ( @@ -119,9 +117,6 @@ func (config *ConfigQemu) defaults() { if config.QemuKVM == nil { config.QemuKVM = util.Pointer(true) } - if config.QemuNetworks == nil { - config.QemuNetworks = QemuDevices{} - } if config.QemuOs == "" { config.QemuOs = "other" } @@ -264,7 +259,7 @@ func (config ConfigQemu) mapToAPI(currentConfig ConfigQemu, version Version) (re config.CreateQemuRngParams(params) // Create networks config. - config.CreateQemuNetworksParams(params) + itemsToDelete += config.Networks.mapToAPI(currentConfig.Networks, params) // Create vga config. vgaParam := QemuDeviceParam{} @@ -433,54 +428,7 @@ func (ConfigQemu) mapToStruct(vmr *VmRef, params map[string]interface{}) (*Confi } } - // Add networks. - nicNames := []string{} - - for k := range params { - if nicName := rxNicName.FindStringSubmatch(k); len(nicName) > 0 { - nicNames = append(nicNames, nicName[0]) - } - } - - if len(nicNames) > 0 { - config.QemuNetworks = QemuDevices{} - for _, nicName := range nicNames { - nicConfStr := params[nicName] - nicConfList := strings.Split(nicConfStr.(string), ",") - - id := rxDeviceID.FindStringSubmatch(nicName) - nicID, _ := strconv.Atoi(id[0]) - model, macaddr := ParseSubConf(nicConfList[0], "=") - - // Add model and MAC address. - nicConfMap := QemuDevice{ - "id": nicID, - "model": model, - "macaddr": macaddr, - } - - // Add rest of device config. - nicConfMap.readDeviceConfig(nicConfList[1:]) - switch nicConfMap["firewall"] { - case 1: - nicConfMap["firewall"] = true - case 0: - nicConfMap["firewall"] = false - } - switch nicConfMap["link_down"] { - case 1: - nicConfMap["link_down"] = true - case 0: - nicConfMap["link_down"] = false - } - - // And device config to networks. - if len(nicConfMap) > 0 { - config.QemuNetworks[nicID] = nicConfMap - } - } - } - + config.Networks = QemuNetworkInterfaces{}.mapToSDK(params) config.Serials = SerialInterfaces{}.mapToSDK(params) // Add usbs @@ -756,6 +704,11 @@ func (config ConfigQemu) Validate(current *ConfigQemu, version Version) (err err return } } + if config.Networks != nil { + if err = config.Networks.Validate(nil); err != nil { + return + } + } if config.TPM != nil { if err = config.TPM.Validate(nil); err != nil { return @@ -772,6 +725,11 @@ func (config ConfigQemu) Validate(current *ConfigQemu, version Version) (err err return } } + if config.Networks != nil { + if err = config.Networks.Validate(current.Networks); err != nil { + return + } + } if config.TPM != nil { if err = config.TPM.Validate(current.TPM); err != nil { return @@ -1123,70 +1081,6 @@ func FormatUsbParam(usb QemuDevice) string { return strings.Join(usbConfParam, ",") } -// Create parameters for each Nic device. -func (c ConfigQemu) CreateQemuNetworksParams(params map[string]interface{}) { - // For new style with multi net device. - for nicID, nicConfMap := range c.QemuNetworks { - - nicConfParam := QemuDeviceParam{} - - // Set Nic name. - qemuNicName := "net" + strconv.Itoa(nicID) - - // Set Mac address. - var macAddr string - switch nicConfMap["macaddr"] { - case nil, "": - // Generate random Mac based on time - macaddr := make(net.HardwareAddr, 6) - r := rand.New(rand.NewSource(time.Now().UnixNano())) - r.Read(macaddr) - macaddr[0] = (macaddr[0] | 2) & 0xfe // fix from github issue #18 - macAddr = strings.ToUpper(fmt.Sprintf("%v", macaddr)) - - // Add Mac to source map so it will be returned. (useful for some use case like Terraform) - nicConfMap["macaddr"] = macAddr - case "repeatable": - // Generate deterministic Mac based on VmID and NicID - // Assume that rare VM has more than 32 nics - macaddr := make(net.HardwareAddr, 6) - pairing := c.VmID<<5 | nicID - // Linux MAC vendor - 00:18:59 - macaddr[0] = 0x00 - macaddr[1] = 0x18 - macaddr[2] = 0x59 - macaddr[3] = byte((pairing >> 16) & 0xff) - macaddr[4] = byte((pairing >> 8) & 0xff) - macaddr[5] = byte(pairing & 0xff) - // Convert to string - macAddr = strings.ToUpper(fmt.Sprintf("%v", macaddr)) - - // Add Mac to source map so it will be returned. (useful for some use case like Terraform) - nicConfMap["macaddr"] = macAddr - default: - macAddr = nicConfMap["macaddr"].(string) - } - - // use model=mac format for older proxmox compatibility as the parameters which will be sent to Proxmox API. - nicConfParam = append(nicConfParam, fmt.Sprintf("%v=%v", nicConfMap["model"], macAddr)) - - // Set bridge if not nat. - if nicConfMap["bridge"].(string) != "nat" { - bridge := fmt.Sprintf("bridge=%v", nicConfMap["bridge"]) - nicConfParam = append(nicConfParam, bridge) - } - - // Keys that are not used as real/direct conf. - ignoredKeys := []string{"id", "bridge", "macaddr", "model"} - - // Rest of config. - nicConfParam = nicConfParam.createDeviceParam(nicConfMap, ignoredKeys) - - // Add nic to Qemu prams. - params[qemuNicName] = strings.Join(nicConfParam, ",") - } -} - // Create RNG parameter. func (c ConfigQemu) CreateQemuRngParams(params map[string]interface{}) { rngParam := QemuDeviceParam{} @@ -1330,49 +1224,3 @@ 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 errors.New(QemuNetworkInterfaceID_Error_Invalid) - } - return nil -} diff --git a/proxmox/config_qemu_cloudinit.go b/proxmox/config_qemu_cloudinit.go index aaf1b7ee..36ad906d 100644 --- a/proxmox/config_qemu_cloudinit.go +++ b/proxmox/config_qemu_cloudinit.go @@ -258,19 +258,19 @@ func (CloudInitCustom) mapToSDK(raw string) *CloudInitCustom { var config CloudInitCustom params := splitStringOfSettings(raw) if v, isSet := params["meta"]; isSet { - config.Meta = CloudInitSnippet{}.mapToSDK(v.(string)) + config.Meta = CloudInitSnippet{}.mapToSDK(v) set = true } if v, isSet := params["network"]; isSet { - config.Network = CloudInitSnippet{}.mapToSDK(v.(string)) + config.Network = CloudInitSnippet{}.mapToSDK(v) set = true } if v, isSet := params["user"]; isSet { - config.User = CloudInitSnippet{}.mapToSDK(v.(string)) + config.User = CloudInitSnippet{}.mapToSDK(v) set = true } if v, isSet := params["vendor"]; isSet { - config.Vendor = CloudInitSnippet{}.mapToSDK(v.(string)) + config.Vendor = CloudInitSnippet{}.mapToSDK(v) set = true } if set { @@ -502,32 +502,32 @@ func (CloudInitNetworkConfig) mapToSDK(param string) (config CloudInitNetworkCon var ipv6 CloudInitIPv6Config if v, isSet := params["ip"]; isSet { ipv4Set = true - if v.(string) == "dhcp" { + if v == "dhcp" { ipv4.DHCP = true } else { - tmp := IPv4CIDR(v.(string)) + tmp := IPv4CIDR(v) ipv4.Address = &tmp } } if v, isSet := params["gw"]; isSet { ipv4Set = true - tmp := IPv4Address(v.(string)) + tmp := IPv4Address(v) ipv4.Gateway = &tmp } if v, isSet := params["ip6"]; isSet { ipv6Set = true - if v.(string) == "dhcp" { + if v == "dhcp" { ipv6.DHCP = true - } else if v.(string) == "auto" { + } else if v == "auto" { ipv6.SLAAC = true } else { - tmp := IPv6CIDR(v.(string)) + tmp := IPv6CIDR(v) ipv6.Address = &tmp } } if v, isSet := params["gw6"]; isSet { ipv6Set = true - tmp := IPv6Address(v.(string)) + tmp := IPv6Address(v) ipv6.Gateway = &tmp } if ipv4Set { diff --git a/proxmox/config_qemu_disk.go b/proxmox/config_qemu_disk.go index 5063787f..98e9df4d 100644 --- a/proxmox/config_qemu_disk.go +++ b/proxmox/config_qemu_disk.go @@ -94,9 +94,9 @@ type qemuCdRom struct { SizeInKibibytes string } -func (qemuCdRom) mapToStruct(diskData string, settings map[string]interface{}) *qemuCdRom { +func (qemuCdRom) mapToStruct(diskData string, settings map[string]string) *qemuCdRom { if setting, isSet := settings["media"]; isSet { - if setting.(string) != "cdrom" { + if setting != "cdrom" { return nil } } else { @@ -128,7 +128,7 @@ func (qemuCdRom) mapToStruct(diskData string, settings map[string]interface{}) * CdRom: true, Storage: tmpStorage[0], File: tmpFile[1], - SizeInKibibytes: setting.(string), + SizeInKibibytes: setting, } } } else { @@ -341,7 +341,7 @@ func (disk qemuDisk) mapToApiValues(vmID, LinkedVmId uint, currentStorage string } // Maps all the disk related settings to our own data structure. -func (qemuDisk) mapToStruct(diskData string, settings map[string]interface{}, linkedVmId *uint) *qemuDisk { +func (qemuDisk) mapToStruct(diskData string, settings map[string]string, linkedVmId *uint) *qemuDisk { disk := qemuDisk{Backup: true} if diskData[0:1] == "/" { @@ -358,79 +358,79 @@ func (qemuDisk) mapToStruct(diskData string, settings map[string]interface{}, li disk.Replicate = true if value, isSet := settings["aio"]; isSet { - disk.AsyncIO = QemuDiskAsyncIO(value.(string)) + disk.AsyncIO = QemuDiskAsyncIO(value) } if value, isSet := settings["backup"]; isSet { - disk.Backup, _ = strconv.ParseBool(value.(string)) + disk.Backup, _ = strconv.ParseBool(value) } if value, isSet := settings["cache"]; isSet { - disk.Cache = QemuDiskCache(value.(string)) + disk.Cache = QemuDiskCache(value) } if value, isSet := settings["discard"]; isSet { - if value.(string) == "on" { + if value == "on" { disk.Discard = true } } if value, isSet := settings["iops_rd"]; isSet { - tmp, _ := strconv.Atoi(value.(string)) + tmp, _ := strconv.Atoi(value) disk.Bandwidth.Iops.ReadLimit.Concurrent = QemuDiskBandwidthIopsLimitConcurrent(tmp) } if value, isSet := settings["iops_rd_max"]; isSet { - tmp, _ := strconv.Atoi(value.(string)) + tmp, _ := strconv.Atoi(value) disk.Bandwidth.Iops.ReadLimit.Burst = QemuDiskBandwidthIopsLimitBurst(tmp) } if value, isSet := settings["iops_rd_max_length"]; isSet { - tmp, _ := strconv.Atoi(value.(string)) + tmp, _ := strconv.Atoi(value) disk.Bandwidth.Iops.ReadLimit.BurstDuration = uint(tmp) } if value, isSet := settings["iops_wr"]; isSet { - tmp, _ := strconv.Atoi(value.(string)) + tmp, _ := strconv.Atoi(value) disk.Bandwidth.Iops.WriteLimit.Concurrent = QemuDiskBandwidthIopsLimitConcurrent(tmp) } if value, isSet := settings["iops_wr_max"]; isSet { - tmp, _ := strconv.Atoi(value.(string)) + tmp, _ := strconv.Atoi(value) disk.Bandwidth.Iops.WriteLimit.Burst = QemuDiskBandwidthIopsLimitBurst(tmp) } if value, isSet := settings["iops_wr_max_length"]; isSet { - tmp, _ := strconv.Atoi(value.(string)) + tmp, _ := strconv.Atoi(value) disk.Bandwidth.Iops.WriteLimit.BurstDuration = uint(tmp) } if value, isSet := settings["iothread"]; isSet { - disk.IOThread, _ = strconv.ParseBool(value.(string)) + disk.IOThread, _ = strconv.ParseBool(value) } if value, isSet := settings["mbps_rd"]; isSet { - tmp, _ := strconv.ParseFloat(value.(string), 32) + tmp, _ := strconv.ParseFloat(value, 32) disk.Bandwidth.MBps.ReadLimit.Concurrent = QemuDiskBandwidthMBpsLimitConcurrent(math.Round(tmp*100) / 100) } if value, isSet := settings["mbps_rd_max"]; isSet { - tmp, _ := strconv.ParseFloat(value.(string), 32) + tmp, _ := strconv.ParseFloat(value, 32) disk.Bandwidth.MBps.ReadLimit.Burst = QemuDiskBandwidthMBpsLimitBurst(math.Round(tmp*100) / 100) } if value, isSet := settings["mbps_wr"]; isSet { - tmp, _ := strconv.ParseFloat(value.(string), 32) + tmp, _ := strconv.ParseFloat(value, 32) disk.Bandwidth.MBps.WriteLimit.Concurrent = QemuDiskBandwidthMBpsLimitConcurrent(math.Round(tmp*100) / 100) } if value, isSet := settings["mbps_wr_max"]; isSet { - tmp, _ := strconv.ParseFloat(value.(string), 32) + tmp, _ := strconv.ParseFloat(value, 32) disk.Bandwidth.MBps.WriteLimit.Burst = QemuDiskBandwidthMBpsLimitBurst(math.Round(tmp*100) / 100) } if value, isSet := settings["replicate"]; isSet { - disk.Replicate, _ = strconv.ParseBool(value.(string)) + disk.Replicate, _ = strconv.ParseBool(value) } if value, isSet := settings["ro"]; isSet { - disk.ReadOnly, _ = strconv.ParseBool(value.(string)) + disk.ReadOnly, _ = strconv.ParseBool(value) } if value, isSet := settings["serial"]; isSet { - disk.Serial = QemuDiskSerial(value.(string)) + disk.Serial = QemuDiskSerial(value) } if value, isSet := settings["size"]; isSet { - disk.SizeInKibibytes = QemuDiskSize(0).parse(value.(string)) + disk.SizeInKibibytes = QemuDiskSize(0).parse(value) } if value, isSet := settings["ssd"]; isSet { - disk.EmulateSSD, _ = strconv.ParseBool(value.(string)) + disk.EmulateSSD, _ = strconv.ParseBool(value) } if value, isSet := settings["wwn"]; isSet { - disk.WorldWideName = QemuWorldWideName(value.(string)) + disk.WorldWideName = QemuWorldWideName(value) } return &disk } diff --git a/proxmox/config_qemu_guestagent.go b/proxmox/config_qemu_guestagent.go index 7337985c..fa11998a 100644 --- a/proxmox/config_qemu_guestagent.go +++ b/proxmox/config_qemu_guestagent.go @@ -53,15 +53,15 @@ func (QemuGuestAgent) mapToSDK(params string) *QemuGuestAgent { config.Enable = &tmpEnable tmpParams := splitStringOfSettings(params) if v, isSet := tmpParams["freeze-fs-on-backup"]; isSet { - tmpBool, _ := strconv.ParseBool(v.(string)) + tmpBool, _ := strconv.ParseBool(v) config.Freeze = &tmpBool } if v, isSet := tmpParams["fstrim_cloned_disks"]; isSet { - tmpBool, _ := strconv.ParseBool(v.(string)) + tmpBool, _ := strconv.ParseBool(v) config.FsTrim = &tmpBool } if v, isSet := tmpParams["type"]; isSet { - config.Type = util.Pointer(QemuGuestAgentType(v.(string))) + config.Type = util.Pointer(QemuGuestAgentType(v)) } return &config } diff --git a/proxmox/config_qemu_network.go b/proxmox/config_qemu_network.go new file mode 100644 index 00000000..656a93ec --- /dev/null +++ b/proxmox/config_qemu_network.go @@ -0,0 +1,534 @@ +package proxmox + +import ( + "errors" + "net" + "slices" + "strconv" + "strings" + + "github.com/Telmate/proxmox-api-go/internal/util" +) + +type QemuMTU struct { + Inherit bool `json:"inherit,omitempty"` + Value MTU `json:"value,omitempty"` +} + +const QemuMTU_Error_Invalid string = "inherit and value are mutually exclusive" + +// unsafe requires caller to check for nil +func (config *QemuMTU) mapToApiUnsafe(builder *strings.Builder) { + if config.Inherit { + builder.WriteString(",mtu=1") + return + } + if config.Value != 0 { + builder.WriteString(",mtu=" + strconv.Itoa(int(config.Value))) + } +} + +func (config QemuMTU) Validate() error { + if config.Inherit { + if config.Value != 0 { + return errors.New(QemuMTU_Error_Invalid) + } + return nil + } + return config.Value.Validate() +} + +const ( + QemuNetworkInterface_Error_BridgeRequired string = "bridge is required during creation" + QemuNetworkInterface_Error_ModelRequired string = "model is required during creation" + QemuNetworkInterface_Error_MtuNoEffect string = "mtu has no effect when model is not virtio" +) + +// if we get more edge cases, we should give every model its own struct +type QemuNetworkInterface struct { + Bridge *string `json:"bridge,omitempty"` // Required for creation + Delete bool `json:"delete,omitempty"` + Connected *bool `json:"connected,omitempty"` + Firewall *bool `json:"firewall,omitempty"` + MAC *net.HardwareAddr `json:"mac,omitempty"` + MTU *QemuMTU `json:"mtu,omitempty"` // only when `Model == QemuNetworkModelVirtIO` + Model *QemuNetworkModel `json:"model,omitempty"` // Required for creation + MultiQueue *QemuNetworkQueue `json:"queue,omitempty"` + RateLimitKBps *QemuNetworkRate `json:"rate,omitempty"` + NativeVlan *Vlan `json:"native_vlan,omitempty"` + TaggedVlans *Vlans `json:"tagged_vlans,omitempty"` +} + +func (config QemuNetworkInterface) mapToApi(current *QemuNetworkInterface) (settings string) { + builder := strings.Builder{} + var mac, model string + if current != nil { // Update + if config.Model != nil { + model = config.Model.String() + } else if current.Model != nil { + model = current.Model.String() + } + builder.WriteString(model) + if config.MAC != nil { + mac = config.MAC.String() + } else if current.MAC != nil { + mac = current.MAC.String() + } + if mac != "" { + builder.WriteString("=" + strings.ToUpper(mac)) + } + if config.Bridge != nil { + builder.WriteString(",bridge=" + *config.Bridge) + } else if current.Bridge != nil { + builder.WriteString(",bridge=" + *current.Bridge) + } + if config.Firewall != nil { + if *config.Firewall { + builder.WriteString(",firewall=" + boolToIntString(*config.Firewall)) + } + } else if current.Firewall != nil && *current.Firewall { + builder.WriteString(",firewall=" + boolToIntString(*current.Firewall)) + } + if config.Connected != nil { + if !*config.Connected { + builder.WriteString(",link_down=" + boolToIntString(!*config.Connected)) + } + } else if current.Connected != nil && !*current.Connected { + builder.WriteString(",link_down=" + boolToIntString(!*current.Connected)) + } + if model == string(QemuNetworkModelVirtIO) { + if config.MTU != nil { + config.MTU.mapToApiUnsafe(&builder) + } else if current.MTU != nil { + current.MTU.mapToApiUnsafe(&builder) + } + } + if config.MultiQueue != nil { + if *config.MultiQueue != 0 { + builder.WriteString(",queues=" + strconv.Itoa(int(*config.MultiQueue))) + } + } else if current.MultiQueue != nil && *current.MultiQueue != 0 { + builder.WriteString(",queues=" + strconv.Itoa(int(*current.MultiQueue))) + } + if config.RateLimitKBps != nil { + config.RateLimitKBps.mapToApiUnsafe(&builder) + } else if current.RateLimitKBps != nil { + current.RateLimitKBps.mapToApiUnsafe(&builder) + } + if config.NativeVlan != nil { + if *config.NativeVlan != 0 { + builder.WriteString(",tag=" + config.NativeVlan.String()) + } + } else if current.NativeVlan != nil && *current.NativeVlan != 0 { + builder.WriteString(",tag=" + current.NativeVlan.String()) + } + if config.TaggedVlans != nil { + vlans := config.TaggedVlans.mapToApiUnsafe() + if vlans != "" { + builder.WriteString(",trunks=" + vlans[1:]) + } + } else if current.TaggedVlans != nil { + vlans := current.TaggedVlans.mapToApiUnsafe() + if vlans != "" { + builder.WriteString(",trunks=" + vlans[1:]) + } + } + return builder.String() + } + // Create + if config.Model != nil { + model = config.Model.String() + } + if config.MAC != nil { + mac = config.MAC.String() + } + if model != "" { + builder.WriteString(model) + if mac != "" { + builder.WriteString("=" + strings.ToUpper(mac)) + } + } + if config.Bridge != nil { + builder.WriteString(",bridge=" + *config.Bridge) + } + if config.Firewall != nil && *config.Firewall { + builder.WriteString(",firewall=" + boolToIntString(*config.Firewall)) + } + if config.Connected != nil && !*config.Connected { + builder.WriteString(",link_down=" + boolToIntString(!*config.Connected)) + } + if config.MTU != nil && model == string(QemuNetworkModelVirtIO) { + config.MTU.mapToApiUnsafe(&builder) + } + if config.MultiQueue != nil && *config.MultiQueue != 0 { + builder.WriteString(",queues=" + strconv.Itoa(int(*config.MultiQueue))) + } + if config.RateLimitKBps != nil { + config.RateLimitKBps.mapToApiUnsafe(&builder) + } + if config.NativeVlan != nil && *config.NativeVlan != 0 { + builder.WriteString(",tag=" + config.NativeVlan.String()) + } + if config.TaggedVlans != nil { + vlans := config.TaggedVlans.mapToApiUnsafe() + if vlans != "" { + builder.WriteString(",trunks=" + vlans[1:]) + } + } + return builder.String() +} + +func (QemuNetworkInterface) mapToSDK(rawParams string) (config QemuNetworkInterface) { + modelAndMac := strings.SplitN(rawParams, ",", 2) + modelAndMac = strings.Split(modelAndMac[0], "=") + var model QemuNetworkModel + if len(modelAndMac) == 2 { + model = QemuNetworkModel(modelAndMac[0]) + config.Model = &model + mac, _ := net.ParseMAC(modelAndMac[1]) + config.MAC = &mac + } + params := splitStringOfSettings(rawParams) + if v, isSet := params["bridge"]; isSet { + config.Bridge = &v + } + if v, isSet := params["link_down"]; isSet { + config.Connected = util.Pointer(v == "0") + } else { + config.Connected = util.Pointer(true) + } + if v, isSet := params["firewall"]; isSet { + config.Firewall = util.Pointer(v == "1") + } else { + config.Firewall = util.Pointer(false) + } + if model == QemuNetworkModelVirtIO { + if v, isSet := params["mtu"]; isSet { + var mtu QemuMTU + if v == "1" { + mtu.Inherit = true + } else { + tmpMtu, _ := strconv.Atoi(v) + mtu.Value = MTU(tmpMtu) + } + config.MTU = &mtu + } + } + if v, isSet := params["queues"]; isSet { + tmpQueue, _ := strconv.Atoi(v) + config.MultiQueue = util.Pointer(QemuNetworkQueue(tmpQueue)) + } + if v, isSet := params["rate"]; isSet { + config.RateLimitKBps = QemuNetworkRate(0).mapToSDK(v) + } + if v, isSet := params["tag"]; isSet { + tmpVlan, _ := strconv.Atoi(v) + config.NativeVlan = util.Pointer(Vlan(tmpVlan)) + } + if v, isSet := params["trunks"]; isSet { + rawVlans := strings.Split(v, ";") + vlans := make(Vlans, len(rawVlans)) + for i, e := range rawVlans { + tmpVlan, _ := strconv.Atoi(e) + vlans[i] = Vlan(tmpVlan) + } + config.TaggedVlans = &vlans + } else { + config.TaggedVlans = &Vlans{} + } + return +} + +func (config QemuNetworkInterface) Validate(current *QemuNetworkInterface) error { + if config.Delete { + return nil + } + var model QemuNetworkModel + if current != nil { // Update + if current.Model != nil { + model = *current.Model + } + } else { // Create + if config.Bridge == nil { + return errors.New(QemuNetworkInterface_Error_BridgeRequired) + } + if config.Model == nil { + return errors.New(QemuNetworkInterface_Error_ModelRequired) + } + } + // shared + if config.Model != nil { + if err := config.Model.Validate(); err != nil { + return err + } + model = QemuNetworkModel((*config.Model).String()) + } + if config.MTU != nil { + if model != QemuNetworkModelVirtIO && (config.MTU.Inherit || config.MTU.Value != 0) { + return errors.New(QemuNetworkInterface_Error_MtuNoEffect) + } + if err := config.MTU.Validate(); err != nil { + return err + } + } + if config.MultiQueue != nil { + if err := config.MultiQueue.Validate(); err != nil { + return err + } + } + if config.RateLimitKBps != nil { + if err := config.RateLimitKBps.Validate(); err != nil { + return err + } + } + if config.NativeVlan != nil { + if err := config.NativeVlan.Validate(); err != nil { + return err + } + } + if config.TaggedVlans != nil { + if err := config.TaggedVlans.Validate(); err != nil { + return err + } + } + return nil +} + +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 + + QemuNetworkInterfaceIDMaximum QemuNetworkInterfaceID = QemuNetworkInterfaceID31 +) + +func (id QemuNetworkInterfaceID) String() string { + return strconv.Itoa(int(id)) +} + +func (id QemuNetworkInterfaceID) Validate() error { + if id > QemuNetworkInterfaceIDMaximum { + return errors.New(QemuNetworkInterfaceID_Error_Invalid) + } + return nil +} + +type QemuNetworkInterfaces map[QemuNetworkInterfaceID]QemuNetworkInterface + +const QemuNetworkInterfacesAmount = uint8(QemuNetworkInterfaceIDMaximum) + 1 + +func (config QemuNetworkInterfaces) mapToAPI(current QemuNetworkInterfaces, params map[string]interface{}) (delete string) { + for i, e := range config { + if v, isSet := current[i]; isSet { // Update + if e.Delete { + delete += ",net" + i.String() + continue + } + params["net"+i.String()] = e.mapToApi(&v) + } else { // Create + if e.Delete { + continue + } + params["net"+i.String()] = e.mapToApi(nil) + } + } + return +} + +func (QemuNetworkInterfaces) mapToSDK(params map[string]interface{}) QemuNetworkInterfaces { + interfaces := QemuNetworkInterfaces{} + for i := uint8(0); i < QemuNetworkInterfacesAmount; i++ { + if rawInterface, isSet := params["net"+strconv.Itoa(int(i))]; isSet { + interfaces[QemuNetworkInterfaceID(i)] = QemuNetworkInterface{}.mapToSDK(rawInterface.(string)) + } + } + if len(interfaces) > 0 { + return interfaces + } + return nil +} + +func (config QemuNetworkInterfaces) Validate(current QemuNetworkInterfaces) error { + for i, e := range config { + if err := i.Validate(); err != nil { + return err + } + var currentInterface *QemuNetworkInterface + if v, isSet := current[i]; isSet { + currentInterface = &v + } + if err := e.Validate(currentInterface); err != nil { + return err + } + } + return nil +} + +type QemuNetworkModel string // enum + +const ( + QemuNetworkModelE1000 QemuNetworkModel = "e1000" + QemuNetworkModelE100082540em QemuNetworkModel = "e1000-82540em" + qemuNetworkModelE100082540em_Lower QemuNetworkModel = "e100082540em" + QemuNetworkModelE100082544gc QemuNetworkModel = "e1000-82544gc" + qemuNetworkModelE100082544gc_Lower QemuNetworkModel = "e100082544gc" + QemuNetworkModelE100082545em QemuNetworkModel = "e1000-82545em" + qemuNetworkModelE100082545em_Lower QemuNetworkModel = "e100082545em" + QemuNetworkModelE1000e QemuNetworkModel = "e1000e" + QemuNetworkModelI82551 QemuNetworkModel = "i82551" + QemuNetworkModelI82557b QemuNetworkModel = "i82557b" + QemuNetworkModelI82559er QemuNetworkModel = "i82559er" + QemuNetworkModelNe2kISA QemuNetworkModel = "ne2k_isa" + qemuNetworkModelNe2kISA_Lower QemuNetworkModel = "ne2kisa" + QemuNetworkModelNe2kPCI QemuNetworkModel = "ne2k_pci" + qemuNetworkModelNe2kPCI_Lower QemuNetworkModel = "ne2kpci" + QemuNetworkModelPcNet QemuNetworkModel = "pcnet" + QemuNetworkModelRtl8139 QemuNetworkModel = "rtl8139" + QemuNetworkModelVirtIO QemuNetworkModel = "virtio" + QemuNetworkModelVmxNet3 QemuNetworkModel = "vmxnet3" +) + +func (QemuNetworkModel) enumMap() map[QemuNetworkModel]QemuNetworkModel { + return map[QemuNetworkModel]QemuNetworkModel{ + QemuNetworkModelE1000: QemuNetworkModelE1000, + qemuNetworkModelE100082540em_Lower: QemuNetworkModelE100082540em, + qemuNetworkModelE100082544gc_Lower: QemuNetworkModelE100082544gc, + qemuNetworkModelE100082545em_Lower: QemuNetworkModelE100082545em, + QemuNetworkModelE1000e: QemuNetworkModelE1000e, + QemuNetworkModelI82551: QemuNetworkModelI82551, + QemuNetworkModelI82557b: QemuNetworkModelI82557b, + QemuNetworkModelI82559er: QemuNetworkModelI82559er, + qemuNetworkModelNe2kISA_Lower: QemuNetworkModelNe2kISA, + qemuNetworkModelNe2kPCI_Lower: QemuNetworkModelNe2kPCI, + QemuNetworkModelPcNet: QemuNetworkModelPcNet, + QemuNetworkModelRtl8139: QemuNetworkModelRtl8139, + QemuNetworkModelVirtIO: QemuNetworkModelVirtIO, + QemuNetworkModelVmxNet3: QemuNetworkModelVmxNet3} +} + +func (QemuNetworkModel) Error() error { + models := QemuNetworkModel("").enumMap() + modelsConverted := make([]string, len(models)) + var index int + for _, e := range models { + modelsConverted[index] = string(e) + index++ + } + slices.Sort(modelsConverted) + return errors.New("qemuNetworkModel can only be one of the following values: " + strings.Join(modelsConverted, ", ")) +} + +// returns the model with proper dashes, underscores and capitalization +func (model QemuNetworkModel) String() string { + models := QemuNetworkModel("").enumMap() + if v, ok := models[QemuNetworkModel(strings.ToLower(strings.ReplaceAll(strings.ReplaceAll(string(model), "_", ""), "-", "")))]; ok { + return string(v) + } + return "" +} + +func (model QemuNetworkModel) Validate() error { + if model.String() != "" { + return nil + } + return QemuNetworkModel("").Error() +} + +type QemuNetworkQueue uint8 // 0-64, 0 to disable + +const ( + QemuNetworkQueueMaximum QemuNetworkQueue = 64 + QemuNetworkQueue_Error_Invalid string = "network queue must be in the range 0-64" +) + +func (queue QemuNetworkQueue) Validate() error { + if queue > QemuNetworkQueueMaximum { + return errors.New(QemuNetworkQueue_Error_Invalid) + } + return nil +} + +type QemuNetworkRate uint32 // 0-10240000 + +const ( + QemuNetworkRate_Error_Invalid string = "network rate must be in the range 0-10240000" + QemuNetworkRateMaximum QemuNetworkRate = 10240000 +) + +// unsafe requires caller to check for nil +func (rate QemuNetworkRate) mapToApiUnsafe(builder *strings.Builder) { + if rate == 0 { + return + } + rawRate := strconv.Itoa(int(rate)) + length := len(rawRate) + switch { + case length > 3: + // Insert a decimal point three places from the end + if rate%1000 == 0 { + builder.WriteString(",rate=" + rawRate[:length-3]) + } else { + builder.WriteString(strings.TrimRight(",rate="+rawRate[:length-3]+"."+rawRate[length-3:], "0")) + } + case length > 0: + // Prepend zeros to ensure decimal places + prefixRate := "000" + rawRate + builder.WriteString(strings.TrimRight(",rate=0."+prefixRate[length:], "0")) + } +} + +func (QemuNetworkRate) mapToSDK(rawRate string) *QemuNetworkRate { + splitRate := strings.Split(rawRate, ".") + var rate int + switch len(splitRate) { + case 1: + if splitRate[0] != "0" { + rate, _ = strconv.Atoi(splitRate[0] + "000") + } + case 2: + // Pad the fractional part to ensure it has at least 3 digits + fractional := splitRate[1] + "000" + rate, _ = strconv.Atoi(splitRate[0] + fractional[:3]) + } + return util.Pointer(QemuNetworkRate(rate)) +} + +func (rate QemuNetworkRate) Validate() error { + if rate > QemuNetworkRateMaximum { + return errors.New(QemuNetworkRate_Error_Invalid) + } + return nil +} diff --git a/proxmox/config_qemu_network_test.go b/proxmox/config_qemu_network_test.go new file mode 100644 index 00000000..d24d5360 --- /dev/null +++ b/proxmox/config_qemu_network_test.go @@ -0,0 +1,345 @@ +package proxmox + +import ( + "errors" + "testing" + + "github.com/Telmate/proxmox-api-go/internal/util" + "github.com/stretchr/testify/require" +) + +func Test_QemuMTU_Validate(t *testing.T) { + tests := []struct { + name string + input QemuMTU + output error + }{ + {name: `Valid inherit`, + input: QemuMTU{Inherit: true}}, + {name: `Valid value`, + input: QemuMTU{Value: 1500}}, + {name: `Valid empty`}, + {name: `Invalid mutually exclusive`, + input: QemuMTU{Inherit: true, Value: 1500}, + output: errors.New(QemuMTU_Error_Invalid)}, + {name: `Invalid too small`, + input: QemuMTU{Value: 575}, + output: errors.New(MTU_Error_Invalid)}, + {name: `Invalid too large`, + input: QemuMTU{Value: 65521}, + output: errors.New(MTU_Error_Invalid)}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require.Equal(t, test.output, test.input.Validate()) + }) + } +} + +func Test_QemuNetworkInterface_Validate(t *testing.T) { + type testInput struct { + config QemuNetworkInterface + current *QemuNetworkInterface + } + tests := []struct { + name string + input testInput + output error + }{ + {name: `Valid Delete`, + input: testInput{ + config: QemuNetworkInterface{Delete: true}}}, + {name: `Valid MTU inherit`, + input: testInput{ + config: QemuNetworkInterface{ + Model: util.Pointer(QemuNetworkModelVirtIO), + MTU: &QemuMTU{Inherit: true}}, + current: &QemuNetworkInterface{}}}, + {name: `Valid MTU value`, + input: testInput{ + config: QemuNetworkInterface{ + Model: util.Pointer(QemuNetworkModelVirtIO), + MTU: &QemuMTU{Value: 1500}}, + current: &QemuNetworkInterface{}}}, + {name: `Valid MTU empty`, + input: testInput{ + config: QemuNetworkInterface{MTU: &QemuMTU{}}, + current: &QemuNetworkInterface{}}}, + {name: `Valid Model`, + input: testInput{ + config: QemuNetworkInterface{Model: util.Pointer(QemuNetworkModel("virtio"))}, + current: &QemuNetworkInterface{}}}, + {name: `Valid MultiQueue`, + input: testInput{ + config: QemuNetworkInterface{MultiQueue: util.Pointer(QemuNetworkQueue(64))}, + current: &QemuNetworkInterface{}}}, + {name: `Valid RateLimitKBps`, + input: testInput{ + config: QemuNetworkInterface{RateLimitKBps: util.Pointer(QemuNetworkRate(10240000))}, + current: &QemuNetworkInterface{}}}, + {name: `Valid NativeVlan`, + input: testInput{ + config: QemuNetworkInterface{NativeVlan: util.Pointer(Vlan(5))}, + current: &QemuNetworkInterface{}}}, + {name: `Valid TaggedVlans`, + input: testInput{ + config: QemuNetworkInterface{TaggedVlans: util.Pointer(Vlans{0, 45, 12, 4095, 12, 45})}, + current: &QemuNetworkInterface{}}, + }, + // Invalid + {name: `Invalid errors.New(QemuNetworkInterface_Error_BridgeRequired)`, + input: testInput{config: QemuNetworkInterface{}}, + output: errors.New(QemuNetworkInterface_Error_BridgeRequired)}, + {name: `Invalid errors.New(QemuNetworkInterface_Error_ModelRequired)`, + input: testInput{config: QemuNetworkInterface{Bridge: util.Pointer("vmbr0")}}, + output: errors.New(QemuNetworkInterface_Error_ModelRequired)}, + {name: `Invalid errors.New(QemuMTU_Error_Invalid)`, + input: testInput{ + config: QemuNetworkInterface{ + Model: util.Pointer(QemuNetworkModelVirtIO), + MTU: &QemuMTU{Inherit: true, Value: 1500}}, + current: &QemuNetworkInterface{}}, + output: errors.New(QemuMTU_Error_Invalid)}, + {name: `Invalid errors.New(MTU_Error_Invalid)`, + input: testInput{ + config: QemuNetworkInterface{ + Model: util.Pointer(QemuNetworkModelVirtIO), + MTU: &QemuMTU{Value: 575}}, + current: &QemuNetworkInterface{}}, + output: errors.New(MTU_Error_Invalid)}, + + {name: `Invalid Model`, + input: testInput{ + config: QemuNetworkInterface{Model: util.Pointer(QemuNetworkModel("invalid"))}, + current: &QemuNetworkInterface{}}, + output: QemuNetworkModel("").Error()}, + {name: `Invalid errors.New(QemuNetworkQueue_Error_Invalid)`, + input: testInput{ + config: QemuNetworkInterface{MultiQueue: util.Pointer(QemuNetworkQueue(65))}, + current: &QemuNetworkInterface{}}, + output: errors.New(QemuNetworkQueue_Error_Invalid)}, + {name: `Invalid errors.New(QemuNetworkRate_Error_Invalid)`, + input: testInput{ + config: QemuNetworkInterface{RateLimitKBps: util.Pointer(QemuNetworkRate(10240001))}, + current: &QemuNetworkInterface{}}, + output: errors.New(QemuNetworkRate_Error_Invalid)}, + {name: `Invalid NativeVlan errors.New(Vlan_Error_Invalid)`, + input: testInput{ + config: QemuNetworkInterface{NativeVlan: util.Pointer(Vlan(4096))}, + current: &QemuNetworkInterface{}}, + output: errors.New(Vlan_Error_Invalid)}, + {name: `Invalid TaggedVlans errors.New(Vlan_Error_Invalid)`, + input: testInput{ + config: QemuNetworkInterface{TaggedVlans: util.Pointer(Vlans{0, 45, 12, 4095, 12, 45, 4096})}, + current: &QemuNetworkInterface{}}, + output: errors.New(Vlan_Error_Invalid)}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require.Equal(t, test.output, test.input.config.Validate(test.input.current)) + }) + } +} + +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()) + }) + } +} + +func Test_QemuNetworkInterfaces_Validate(t *testing.T) { + type testInput struct { + config QemuNetworkInterfaces + current QemuNetworkInterfaces + } + tests := []struct { + name string + input testInput + output error + }{ + {name: `Valid Delete`, + input: testInput{ + config: QemuNetworkInterfaces{QemuNetworkInterfaceID0: QemuNetworkInterface{Delete: true}}}}, + {name: `Valid MTU inherit`, + input: testInput{ + config: QemuNetworkInterfaces{QemuNetworkInterfaceID0: QemuNetworkInterface{ + Model: util.Pointer(QemuNetworkModelVirtIO), + MTU: &QemuMTU{Inherit: true}}}, + current: QemuNetworkInterfaces{QemuNetworkInterfaceID0: QemuNetworkInterface{}}}}, + {name: `Valid MTU value`, + input: testInput{ + config: QemuNetworkInterfaces{QemuNetworkInterfaceID1: QemuNetworkInterface{ + Model: util.Pointer(QemuNetworkModelVirtIO), + MTU: &QemuMTU{Value: 1500}}}, + current: QemuNetworkInterfaces{QemuNetworkInterfaceID1: QemuNetworkInterface{}}}}, + {name: `Valid MTU empty`, + input: testInput{ + config: QemuNetworkInterfaces{QemuNetworkInterfaceID2: QemuNetworkInterface{ + MTU: &QemuMTU{}}}, + current: QemuNetworkInterfaces{QemuNetworkInterfaceID2: QemuNetworkInterface{}}}}, + {name: `Valid Model`, + input: testInput{ + config: QemuNetworkInterfaces{QemuNetworkInterfaceID3: QemuNetworkInterface{ + Model: util.Pointer(QemuNetworkModel("virtio"))}}, + current: QemuNetworkInterfaces{QemuNetworkInterfaceID3: QemuNetworkInterface{}}}}, + {name: `Valid MultiQueue`, + input: testInput{ + config: QemuNetworkInterfaces{QemuNetworkInterfaceID4: QemuNetworkInterface{ + MultiQueue: util.Pointer(QemuNetworkQueue(64))}}, + current: QemuNetworkInterfaces{QemuNetworkInterfaceID4: QemuNetworkInterface{}}}}, + {name: `Valid RateLimitKBps`, + input: testInput{ + config: QemuNetworkInterfaces{QemuNetworkInterfaceID5: QemuNetworkInterface{ + RateLimitKBps: util.Pointer(QemuNetworkRate(10240000))}}, + current: QemuNetworkInterfaces{QemuNetworkInterfaceID5: QemuNetworkInterface{}}}}, + {name: `Valid NativeVlan`, + input: testInput{ + config: QemuNetworkInterfaces{QemuNetworkInterfaceID6: QemuNetworkInterface{ + NativeVlan: util.Pointer(Vlan(5))}}, + current: QemuNetworkInterfaces{QemuNetworkInterfaceID6: QemuNetworkInterface{}}}}, + {name: `Valid TaggedVlans`, + input: testInput{ + config: QemuNetworkInterfaces{QemuNetworkInterfaceID7: QemuNetworkInterface{ + TaggedVlans: util.Pointer(Vlans{0, 45, 12, 4095, 12, 45})}}, + current: QemuNetworkInterfaces{QemuNetworkInterfaceID7: QemuNetworkInterface{}}}}, + // Invalid + {name: `Invalid errors.New(QemuNetworkInterfaceID_Error_Invalid)`, + input: testInput{config: QemuNetworkInterfaces{32: QemuNetworkInterface{}}}, + output: errors.New(QemuNetworkInterfaceID_Error_Invalid)}, + {name: `Invalid errors.New(QemuNetworkInterface_Error_BridgeRequired)`, + input: testInput{config: QemuNetworkInterfaces{QemuNetworkInterfaceID8: QemuNetworkInterface{}}}, + output: errors.New(QemuNetworkInterface_Error_BridgeRequired)}, + {name: `Invalid errors.New(QemuNetworkInterface_Error_ModelRequired)`, + input: testInput{config: QemuNetworkInterfaces{QemuNetworkInterfaceID8: QemuNetworkInterface{ + Bridge: util.Pointer("vmbr0")}}}, + output: errors.New(QemuNetworkInterface_Error_ModelRequired)}, + {name: `Invalid errors.New(MTU_Error_Invalid)`, + input: testInput{ + config: QemuNetworkInterfaces{QemuNetworkInterfaceID9: QemuNetworkInterface{ + Model: util.Pointer(QemuNetworkModelVirtIO), + MTU: &QemuMTU{Value: 575}}}, + current: QemuNetworkInterfaces{QemuNetworkInterfaceID9: QemuNetworkInterface{}}}, + output: errors.New(MTU_Error_Invalid)}, + {name: `Invalid errors.New(QemuMTU_Error_Invalid)`, + input: testInput{ + config: QemuNetworkInterfaces{QemuNetworkInterfaceID10: QemuNetworkInterface{ + Model: util.Pointer(QemuNetworkModelVirtIO), + MTU: &QemuMTU{Inherit: true, Value: 1500}}}, + current: QemuNetworkInterfaces{QemuNetworkInterfaceID10: QemuNetworkInterface{}}}, + output: errors.New(QemuMTU_Error_Invalid)}, + {name: `Invalid Model`, + input: testInput{ + config: QemuNetworkInterfaces{QemuNetworkInterfaceID11: QemuNetworkInterface{ + Model: util.Pointer(QemuNetworkModel("invalid"))}}, + current: QemuNetworkInterfaces{QemuNetworkInterfaceID11: QemuNetworkInterface{}}}, + output: QemuNetworkModel("").Error()}, + {name: `Invalid errors.New(QemuNetworkQueue_Error_Invalid)`, + input: testInput{ + config: QemuNetworkInterfaces{QemuNetworkInterfaceID12: QemuNetworkInterface{ + MultiQueue: util.Pointer(QemuNetworkQueueMaximum + 1)}}, + current: QemuNetworkInterfaces{QemuNetworkInterfaceID12: QemuNetworkInterface{}}}, + output: errors.New(QemuNetworkQueue_Error_Invalid)}, + {name: `Invalid errors.New(QemuNetworkRate_Error_Invalid)`, + input: testInput{ + config: QemuNetworkInterfaces{QemuNetworkInterfaceID13: QemuNetworkInterface{ + RateLimitKBps: util.Pointer(QemuNetworkRate(10240001))}}, + current: QemuNetworkInterfaces{QemuNetworkInterfaceID13: QemuNetworkInterface{}}}, + output: errors.New(QemuNetworkRate_Error_Invalid)}, + {name: `Invalid NativeVlan errors.New(Vlan_Error_Invalid)`, + input: testInput{ + config: QemuNetworkInterfaces{QemuNetworkInterfaceID14: QemuNetworkInterface{ + NativeVlan: util.Pointer(Vlan(4096))}}, + current: QemuNetworkInterfaces{QemuNetworkInterfaceID14: QemuNetworkInterface{}}}, + output: errors.New(Vlan_Error_Invalid)}, + {name: `Invalid TaggedVlans errors.New(Vlan_Error_Invalid)`, + input: testInput{ + config: QemuNetworkInterfaces{QemuNetworkInterfaceID15: QemuNetworkInterface{ + TaggedVlans: util.Pointer(Vlans{0, 45, 12, 4095, 12, 45, 4096})}}, + current: QemuNetworkInterfaces{QemuNetworkInterfaceID15: QemuNetworkInterface{}}}, + output: errors.New(Vlan_Error_Invalid)}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require.Equal(t, test.output, test.input.config.Validate(test.input.current)) + }) + } +} + +func Test_QemuNetworkModel_Validate(t *testing.T) { + tests := []struct { + name string + input QemuNetworkModel + output error + }{ + {name: `Valid weird`, + input: "E__1--0__-__00-8__2--__--545_Em__"}, + {name: `Valid normal`, + input: QemuNetworkModelE100082544gc}, + {name: `Invalid`, + input: "invalid", + output: QemuNetworkModel("").Error()}, + {name: `Invalid empty`, + input: "", + output: QemuNetworkModel("").Error()}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require.Equal(t, test.output, test.input.Validate()) + }) + } +} + +func Test_QemuNetworkQueue_Validate(t *testing.T) { + tests := []struct { + name string + input QemuNetworkQueue + output error + }{ + {name: `Valid Minimum`, + input: 0}, + {name: `Valid Maximum`, + input: 64}, + {name: `Invalid`, + input: 65, + output: errors.New(QemuNetworkQueue_Error_Invalid)}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require.Equal(t, test.output, test.input.Validate()) + }) + } +} + +func Test_QemuNetworkRate_Validate(t *testing.T) { + tests := []struct { + name string + input QemuNetworkRate + output error + }{ + {name: `Valid maximum`, + input: 10240000}, + {name: `Valid minimum`, + input: 0}, + {name: `Invalid`, + input: 10240001, + output: errors.New(QemuNetworkRate_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 e0b264c9..d61b5102 100644 --- a/proxmox/config_qemu_test.go +++ b/proxmox/config_qemu_test.go @@ -3,6 +3,7 @@ package proxmox import ( "crypto" "errors" + "net" "net/netip" "testing" @@ -42,6 +43,23 @@ func Test_ConfigQemu_mapToAPI(t *testing.T) { ip, _ = netip.ParseAddr(rawIP) return } + parseMAC := func(rawMAC string) (mac net.HardwareAddr) { + mac, _ = net.ParseMAC(rawMAC) + return + } + networkInterface := func() QemuNetworkInterface { + return QemuNetworkInterface{ + Bridge: util.Pointer("vmbr0"), + Connected: util.Pointer(false), + Firewall: util.Pointer(true), + MAC: util.Pointer(parseMAC("52:54:00:12:34:56")), + MTU: util.Pointer(QemuMTU{Value: 1500}), + Model: util.Pointer(QemuNetworkModel("virtio")), + MultiQueue: util.Pointer(QemuNetworkQueue(5)), + RateLimitKBps: util.Pointer(QemuNetworkRate(45)), + NativeVlan: util.Pointer(Vlan(23)), + TaggedVlans: util.Pointer(Vlans{12, 23, 45})} + } format_Raw := QemuDiskFormat_Raw float10 := QemuDiskBandwidthMBpsLimitConcurrent(10.3) float45 := QemuDiskBandwidthMBpsLimitConcurrent(45.23) @@ -3478,6 +3496,176 @@ func Test_ConfigQemu_mapToAPI(t *testing.T) { config: &ConfigQemu{Memory: &QemuMemory{Shares: util.Pointer(QemuMemoryShares(0))}}, currentConfig: ConfigQemu{Memory: &QemuMemory{Shares: util.Pointer(QemuMemoryShares(20000))}}, output: map[string]interface{}{"delete": "shares"}}}}, + {category: `Networks`, + create: []test{ + {name: `Delete`, + config: &ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID0: QemuNetworkInterface{ + Bridge: util.Pointer("vmbr0"), + Connected: util.Pointer(true), + Delete: true, + Firewall: util.Pointer(true), + MAC: util.Pointer(net.HardwareAddr("00:11:22:33:44:55")), + Model: util.Pointer(QemuNetworkModelVirtIO), + MultiQueue: util.Pointer(QemuNetworkQueue(4)), + RateLimitKBps: util.Pointer(QemuNetworkRate(45)), + NativeVlan: util.Pointer(Vlan(23))}}}, + output: map[string]interface{}{}}}, + createUpdate: []test{ + {name: `Bridge`, + config: &ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID0: QemuNetworkInterface{Bridge: util.Pointer("vmbr0")}}}, + currentConfig: ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID0: QemuNetworkInterface{Bridge: util.Pointer("vmbr1")}}}, + output: map[string]interface{}{"net0": ",bridge=vmbr0"}}, + {name: `Connected true`, + config: &ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID1: QemuNetworkInterface{Connected: util.Pointer(true)}}}, + currentConfig: ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID1: QemuNetworkInterface{Connected: util.Pointer(false)}}}, + output: map[string]interface{}{"net1": ""}}, + {name: `Connected false`, + config: &ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID2: QemuNetworkInterface{Connected: util.Pointer(false)}}}, + currentConfig: ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID2: QemuNetworkInterface{Connected: util.Pointer(true)}}}, + output: map[string]interface{}{"net2": ",link_down=1"}}, + {name: `Firewall true`, + config: &ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID3: QemuNetworkInterface{Firewall: util.Pointer(true)}}}, + currentConfig: ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID3: QemuNetworkInterface{Firewall: util.Pointer(false)}}}, + output: map[string]interface{}{"net3": ",firewall=1"}}, + {name: `Firewall false`, + config: &ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID4: QemuNetworkInterface{Firewall: util.Pointer(false)}}}, + currentConfig: ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID4: QemuNetworkInterface{Firewall: util.Pointer(true)}}}, + output: map[string]interface{}{"net4": ""}}, + {name: `MAC`, + config: &ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID5: QemuNetworkInterface{ + Model: util.Pointer(QemuNetworkModelE1000), + MAC: util.Pointer(net.HardwareAddr(parseMAC("BC:11:22:33:44:55")))}}}, + currentConfig: ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID5: QemuNetworkInterface{ + Model: util.Pointer(QemuNetworkModelVirtIO), + MAC: util.Pointer(net.HardwareAddr(parseMAC("bc:11:22:33:44:56")))}}}, + output: map[string]interface{}{"net5": "e1000=BC:11:22:33:44:55"}}, + {name: `MTU.Inherit model=virtio`, + config: &ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID6: QemuNetworkInterface{ + Model: util.Pointer(QemuNetworkModelVirtIO), + MTU: util.Pointer(QemuMTU{Inherit: true})}}}, + currentConfig: ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID6: QemuNetworkInterface{MTU: util.Pointer(QemuMTU{Value: MTU(1500)})}}}, + output: map[string]interface{}{"net6": "virtio,mtu=1"}}, + {name: `MTU.Value model=none`, + config: &ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID7: QemuNetworkInterface{MTU: util.Pointer(QemuMTU{Value: MTU(1400)})}}}, + currentConfig: ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID7: QemuNetworkInterface{MTU: util.Pointer(QemuMTU{Value: MTU(1500)})}}}, + output: map[string]interface{}{"net7": ""}}, + {name: `MTU.Value model=virtio`, + config: &ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID7: QemuNetworkInterface{ + Model: util.Pointer(QemuNetworkModelVirtIO), + MTU: util.Pointer(QemuMTU{Value: MTU(1400)})}}}, + currentConfig: ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID7: QemuNetworkInterface{MTU: util.Pointer(QemuMTU{Value: MTU(1500)})}}}, + output: map[string]interface{}{"net7": "virtio,mtu=1400"}}, + {name: `MTU.Value=0 model=virtio`, + config: &ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID8: QemuNetworkInterface{ + Model: util.Pointer(QemuNetworkModelVirtIO), + MTU: util.Pointer(QemuMTU{Value: MTU(0)})}}}, + currentConfig: ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID8: QemuNetworkInterface{MTU: util.Pointer(QemuMTU{})}}}, + output: map[string]interface{}{"net8": "virtio"}}, + {name: `Model`, + config: &ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID9: QemuNetworkInterface{Model: util.Pointer(qemuNetworkModelE100082544gc_Lower)}}}, + currentConfig: ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID9: QemuNetworkInterface{Model: util.Pointer(QemuNetworkModelVirtIO)}}}, + output: map[string]interface{}{"net9": "e1000-82544gc"}}, + {name: `Model invalid`, + config: &ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID10: QemuNetworkInterface{Model: util.Pointer(QemuNetworkModel("gibberish"))}}}, + currentConfig: ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID10: QemuNetworkInterface{Model: util.Pointer(QemuNetworkModelVirtIO)}}}, + output: map[string]interface{}{"net10": ""}}, + {name: `MultiQueue set`, + config: &ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID11: QemuNetworkInterface{MultiQueue: util.Pointer(QemuNetworkQueue(4))}}}, + currentConfig: ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID11: QemuNetworkInterface{MultiQueue: util.Pointer(QemuNetworkQueue(2))}}}, + output: map[string]interface{}{"net11": ",queues=4"}}, + {name: `MultiQueue unset`, + config: &ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID12: QemuNetworkInterface{MultiQueue: util.Pointer(QemuNetworkQueue(0))}}}, + currentConfig: ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID12: QemuNetworkInterface{MultiQueue: util.Pointer(QemuNetworkQueue(2))}}}, + output: map[string]interface{}{"net12": ""}}, + {name: `RateLimitKBps 0`, + config: &ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID13: QemuNetworkInterface{RateLimitKBps: util.Pointer(QemuNetworkRate(0))}}}, + currentConfig: ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID13: QemuNetworkInterface{RateLimitKBps: util.Pointer(QemuNetworkRate(5))}}}, + output: map[string]interface{}{"net13": ""}}, + {name: `RateLimitKBps 0.007`, + config: &ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID13: QemuNetworkInterface{RateLimitKBps: util.Pointer(QemuNetworkRate(7))}}}, + currentConfig: ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID13: QemuNetworkInterface{RateLimitKBps: util.Pointer(QemuNetworkRate(5))}}}, + output: map[string]interface{}{"net13": ",rate=0.007"}}, + {name: `RateLimitKBps 0.07`, + config: &ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID14: QemuNetworkInterface{RateLimitKBps: util.Pointer(QemuNetworkRate(70))}}}, + currentConfig: ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID14: QemuNetworkInterface{RateLimitKBps: util.Pointer(QemuNetworkRate(5))}}}, + output: map[string]interface{}{"net14": ",rate=0.07"}}, + {name: `RateLimitKBps 0.7`, + config: &ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID15: QemuNetworkInterface{RateLimitKBps: util.Pointer(QemuNetworkRate(700))}}}, + currentConfig: ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID15: QemuNetworkInterface{RateLimitKBps: util.Pointer(QemuNetworkRate(5))}}}, + output: map[string]interface{}{"net15": ",rate=0.7"}}, + {name: `RateLimitKBps 7`, + config: &ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID16: QemuNetworkInterface{RateLimitKBps: util.Pointer(QemuNetworkRate(7000))}}}, + currentConfig: ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID16: QemuNetworkInterface{RateLimitKBps: util.Pointer(QemuNetworkRate(5))}}}, + output: map[string]interface{}{"net16": ",rate=7"}}, + {name: `RateLimitKBps 7.546`, + config: &ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID17: QemuNetworkInterface{RateLimitKBps: util.Pointer(QemuNetworkRate(7546))}}}, + currentConfig: ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID17: QemuNetworkInterface{RateLimitKBps: util.Pointer(QemuNetworkRate(5))}}}, + output: map[string]interface{}{"net17": ",rate=7.546"}}, + {name: `RateLimitKBps 734.546`, + config: &ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID18: QemuNetworkInterface{RateLimitKBps: util.Pointer(QemuNetworkRate(734546))}}}, + currentConfig: ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID18: QemuNetworkInterface{RateLimitKBps: util.Pointer(QemuNetworkRate(5))}}}, + output: map[string]interface{}{"net18": ",rate=734.546"}}, + {name: `NativeVlan unset`, + config: &ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID19: QemuNetworkInterface{NativeVlan: util.Pointer(Vlan(0))}}}, + currentConfig: ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID19: QemuNetworkInterface{NativeVlan: util.Pointer(Vlan(2))}}}, + output: map[string]interface{}{"net19": ""}}, + {name: `NativeVlan set`, + config: &ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID20: QemuNetworkInterface{NativeVlan: util.Pointer(Vlan(83))}}}, + currentConfig: ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID20: QemuNetworkInterface{NativeVlan: util.Pointer(Vlan(2))}}}, + output: map[string]interface{}{"net20": ",tag=83"}}, + {name: `TaggedVlans set`, + config: &ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID21: QemuNetworkInterface{TaggedVlans: util.Pointer(Vlans{10, 43, 23})}}}, + currentConfig: ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID21: QemuNetworkInterface{TaggedVlans: util.Pointer(Vlans{12, 56})}}}, + output: map[string]interface{}{"net21": ",trunks=10;43;23"}}, + {name: `TaggedVlans unset`, + config: &ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID22: QemuNetworkInterface{TaggedVlans: util.Pointer(Vlans{})}}}, + currentConfig: ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID22: QemuNetworkInterface{TaggedVlans: util.Pointer(Vlans{12, 56})}}}, + output: map[string]interface{}{"net22": ""}}}, + update: []test{ + {name: `Bridge replace`, + config: &ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID31: QemuNetworkInterface{Bridge: util.Pointer("vmbr45")}}}, + currentConfig: ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID31: networkInterface()}}, + output: map[string]interface{}{"net31": "virtio=52:54:00:12:34:56,bridge=vmbr45,firewall=1,link_down=1,mtu=1500,queues=5,rate=0.045,tag=23,trunks=12;23;45"}}, + {name: `Connected replace`, + config: &ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID30: QemuNetworkInterface{Connected: util.Pointer(true)}}}, + currentConfig: ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID30: networkInterface()}}, + output: map[string]interface{}{"net30": "virtio=52:54:00:12:34:56,bridge=vmbr0,firewall=1,mtu=1500,queues=5,rate=0.045,tag=23,trunks=12;23;45"}}, + {name: `Firewall replace`, + config: &ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID29: QemuNetworkInterface{Firewall: util.Pointer(false)}}}, + currentConfig: ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID29: networkInterface()}}, + output: map[string]interface{}{"net29": "virtio=52:54:00:12:34:56,bridge=vmbr0,link_down=1,mtu=1500,queues=5,rate=0.045,tag=23,trunks=12;23;45"}}, + {name: `MAC replace`, + config: &ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID28: QemuNetworkInterface{MAC: util.Pointer(net.HardwareAddr(parseMAC("00:11:22:33:44:55")))}}}, + currentConfig: ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID28: networkInterface()}}, + output: map[string]interface{}{"net28": "virtio=00:11:22:33:44:55,bridge=vmbr0,firewall=1,link_down=1,mtu=1500,queues=5,rate=0.045,tag=23,trunks=12;23;45"}}, + {name: `MTU replace`, + config: &ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID27: QemuNetworkInterface{MTU: util.Pointer(QemuMTU{Value: MTU(1400)})}}}, + currentConfig: ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID27: networkInterface()}}, + output: map[string]interface{}{"net27": "virtio=52:54:00:12:34:56,bridge=vmbr0,firewall=1,link_down=1,mtu=1400,queues=5,rate=0.045,tag=23,trunks=12;23;45"}}, + {name: `Model replace`, + config: &ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID26: QemuNetworkInterface{Model: util.Pointer(qemuNetworkModelE100082544gc_Lower)}}}, + currentConfig: ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID26: networkInterface()}}, + output: map[string]interface{}{"net26": "e1000-82544gc=52:54:00:12:34:56,bridge=vmbr0,firewall=1,link_down=1,queues=5,rate=0.045,tag=23,trunks=12;23;45"}}, + {name: `MultiQueue replace`, + config: &ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID25: QemuNetworkInterface{MultiQueue: util.Pointer(QemuNetworkQueue(4))}}}, + currentConfig: ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID25: networkInterface()}}, + output: map[string]interface{}{"net25": "virtio=52:54:00:12:34:56,bridge=vmbr0,firewall=1,link_down=1,mtu=1500,queues=4,rate=0.045,tag=23,trunks=12;23;45"}}, + {name: `RateLimitKBps replace`, + config: &ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID24: QemuNetworkInterface{RateLimitKBps: util.Pointer(QemuNetworkRate(539))}}}, + currentConfig: ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID24: networkInterface()}}, + output: map[string]interface{}{"net24": "virtio=52:54:00:12:34:56,bridge=vmbr0,firewall=1,link_down=1,mtu=1500,queues=5,rate=0.539,tag=23,trunks=12;23;45"}}, + {name: `NaitiveVlan replace`, + config: &ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID23: QemuNetworkInterface{NativeVlan: util.Pointer(Vlan(0))}}}, + currentConfig: ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID23: networkInterface()}}, + output: map[string]interface{}{"net23": "virtio=52:54:00:12:34:56,bridge=vmbr0,firewall=1,link_down=1,mtu=1500,queues=5,rate=0.045,trunks=12;23;45"}}, + {name: `TaggedVlans replace`, + config: &ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID22: QemuNetworkInterface{TaggedVlans: util.Pointer(Vlans{10, 70, 18})}}}, + currentConfig: ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID22: networkInterface()}}, + output: map[string]interface{}{"net22": "virtio=52:54:00:12:34:56,bridge=vmbr0,firewall=1,link_down=1,mtu=1500,queues=5,rate=0.045,tag=23,trunks=10;70;18"}}, + {name: `Delete`, + config: &ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID21: QemuNetworkInterface{Delete: true}}}, + currentConfig: ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID21: QemuNetworkInterface{}}}, + output: map[string]interface{}{"delete": "net21"}}}}, {category: `Serials`, createUpdate: []test{ {name: `delete non existing`, @@ -3597,6 +3785,10 @@ func Test_ConfigQemu_mapToStruct(t *testing.T) { ip, _ = netip.ParseAddr(rawIP) return } + parseMAC := func(rawMAC string) (mac net.HardwareAddr) { + mac, _ = net.ParseMAC(rawMAC) + return + } uint1 := uint(1) uint2 := uint(2) uint31 := uint(31) @@ -5952,6 +6144,215 @@ func Test_ConfigQemu_mapToStruct(t *testing.T) { {name: `shares`, input: map[string]interface{}{"shares": float64(100)}, output: baseConfig(ConfigQemu{Memory: &QemuMemory{Shares: util.Pointer(QemuMemoryShares(100))}})}}}, + {category: `Networks`, + tests: []test{ + {name: `all e1000`, + input: map[string]interface{}{"net0": "e1000=BC:24:11:E1:BB:5D,bridge=vmbr0,mtu=1395,firewall=1,link_down=1,queues=23,rate=1.53,tag=12,trunks=34;18;25"}, + output: baseConfig(ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID0: QemuNetworkInterface{ + Bridge: util.Pointer("vmbr0"), + Connected: util.Pointer(false), + Firewall: util.Pointer(true), + MAC: util.Pointer(parseMAC("BC:24:11:E1:BB:5D")), + // MTU is only supported for virtio + Model: util.Pointer(QemuNetworkModelE1000), + MultiQueue: util.Pointer(QemuNetworkQueue(23)), + RateLimitKBps: util.Pointer(QemuNetworkRate(1530)), + NativeVlan: util.Pointer(Vlan(12)), + TaggedVlans: util.Pointer(Vlans{34, 18, 25})}}})}, + {name: `all virtio`, + input: map[string]interface{}{"net31": "virtio=BC:24:11:E1:BB:5D,bridge=vmbr0,mtu=1395,firewall=1,link_down=1,queues=23,rate=1.53,tag=12,trunks=34;18;25"}, + output: baseConfig(ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID31: QemuNetworkInterface{ + Bridge: util.Pointer("vmbr0"), + Connected: util.Pointer(false), + Firewall: util.Pointer(true), + MAC: util.Pointer(parseMAC("BC:24:11:E1:BB:5D")), + MTU: util.Pointer(QemuMTU{Value: 1395}), + Model: util.Pointer(QemuNetworkModelVirtIO), + MultiQueue: util.Pointer(QemuNetworkQueue(23)), + RateLimitKBps: util.Pointer(QemuNetworkRate(1530)), + NativeVlan: util.Pointer(Vlan(12)), + TaggedVlans: util.Pointer(Vlans{34, 18, 25})}}})}, + {name: `Bridge`, + input: map[string]interface{}{"net2": "virtio=BC:24:11:E1:BB:5D,bridge=vmbr0"}, + output: baseConfig(ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID2: QemuNetworkInterface{ + Bridge: util.Pointer("vmbr0"), + Connected: util.Pointer(true), + Firewall: util.Pointer(false), + MAC: util.Pointer(parseMAC("BC:24:11:E1:BB:5D")), + Model: util.Pointer(QemuNetworkModelVirtIO), + TaggedVlans: util.Pointer(Vlans{})}}})}, + {name: `Model and Mac`, + input: map[string]interface{}{"net3": "virtio=BC:24:11:E1:BB:5D"}, + output: baseConfig(ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID3: QemuNetworkInterface{ + Connected: util.Pointer(true), + Firewall: util.Pointer(false), + MAC: util.Pointer(parseMAC("BC:24:11:E1:BB:5D")), + Model: util.Pointer(QemuNetworkModelVirtIO), + TaggedVlans: util.Pointer(Vlans{})}}})}, + {name: `Connected false`, + input: map[string]interface{}{"net4": "virtio=BC:24:11:E1:BB:5D,link_down=1"}, + output: baseConfig(ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID4: QemuNetworkInterface{ + Connected: util.Pointer(false), + Firewall: util.Pointer(false), + MAC: util.Pointer(parseMAC("BC:24:11:E1:BB:5D")), + Model: util.Pointer(QemuNetworkModelVirtIO), + TaggedVlans: util.Pointer(Vlans{})}}})}, + {name: `Connected true`, + input: map[string]interface{}{"net5": "virtio=BC:24:11:E1:BB:5D,link_down=0"}, + output: baseConfig(ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID5: QemuNetworkInterface{ + Connected: util.Pointer(true), + Firewall: util.Pointer(false), + MAC: util.Pointer(parseMAC("BC:24:11:E1:BB:5D")), + Model: util.Pointer(QemuNetworkModelVirtIO), + TaggedVlans: util.Pointer(Vlans{})}}})}, + {name: `Connected unset`, + input: map[string]interface{}{"net6": "virtio=BC:24:11:E1:BB:5D"}, + output: baseConfig(ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID6: QemuNetworkInterface{ + Connected: util.Pointer(true), + Firewall: util.Pointer(false), + MAC: util.Pointer(parseMAC("BC:24:11:E1:BB:5D")), + Model: util.Pointer(QemuNetworkModelVirtIO), + TaggedVlans: util.Pointer(Vlans{})}}})}, + {name: `Firwall true`, + input: map[string]interface{}{"net7": "virtio=BC:24:11:E1:BB:5D,firewall=1"}, + output: baseConfig(ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID7: QemuNetworkInterface{ + Connected: util.Pointer(true), + Firewall: util.Pointer(true), + MAC: util.Pointer(parseMAC("BC:24:11:E1:BB:5D")), + Model: util.Pointer(QemuNetworkModelVirtIO), + TaggedVlans: util.Pointer(Vlans{})}}})}, + {name: `Firwall false`, + input: map[string]interface{}{"net8": "virtio=BC:24:11:E1:BB:5D,firewall=0"}, + output: baseConfig(ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID8: QemuNetworkInterface{ + Connected: util.Pointer(true), + Firewall: util.Pointer(false), + MAC: util.Pointer(parseMAC("BC:24:11:E1:BB:5D")), + Model: util.Pointer(QemuNetworkModelVirtIO), + TaggedVlans: util.Pointer(Vlans{})}}})}, + {name: `Firwall unset`, + input: map[string]interface{}{"net9": "virtio=BC:24:11:E1:BB:5D"}, + output: baseConfig(ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID9: QemuNetworkInterface{ + Connected: util.Pointer(true), + Firewall: util.Pointer(false), + MAC: util.Pointer(parseMAC("BC:24:11:E1:BB:5D")), + Model: util.Pointer(QemuNetworkModelVirtIO), + TaggedVlans: util.Pointer(Vlans{})}}})}, + {name: `MTU value`, + input: map[string]interface{}{"net10": "virtio=BC:24:11:E1:BB:5D,mtu=1500"}, + output: baseConfig(ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID10: QemuNetworkInterface{ + Connected: util.Pointer(true), + Firewall: util.Pointer(false), + MAC: util.Pointer(parseMAC("BC:24:11:E1:BB:5D")), + MTU: &QemuMTU{Value: 1500}, + Model: util.Pointer(QemuNetworkModelVirtIO), + TaggedVlans: util.Pointer(Vlans{})}}})}, + {name: `MTU inherit`, + input: map[string]interface{}{"net11": "virtio=BC:24:11:E1:BB:5D,mtu=1"}, + output: baseConfig(ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID11: QemuNetworkInterface{ + Connected: util.Pointer(true), + Firewall: util.Pointer(false), + MAC: util.Pointer(parseMAC("BC:24:11:E1:BB:5D")), + MTU: &QemuMTU{Inherit: true}, + Model: util.Pointer(QemuNetworkModelVirtIO), + TaggedVlans: util.Pointer(Vlans{})}}})}, + {name: `MultiQueue disable`, + input: map[string]interface{}{"net12": "virtio=BC:24:11:E1:BB:5D"}, + output: baseConfig(ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID12: QemuNetworkInterface{ + Connected: util.Pointer(true), + Firewall: util.Pointer(false), + MAC: util.Pointer(parseMAC("BC:24:11:E1:BB:5D")), + Model: util.Pointer(QemuNetworkModelVirtIO), + TaggedVlans: util.Pointer(Vlans{})}}})}, + {name: `MultiQueue enable`, + input: map[string]interface{}{"net0": "virtio=BC:24:11:E1:BB:5D,queues=1"}, + output: baseConfig(ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID0: QemuNetworkInterface{ + Connected: util.Pointer(true), + Firewall: util.Pointer(false), + MultiQueue: util.Pointer(QemuNetworkQueue(1)), + MAC: util.Pointer(parseMAC("BC:24:11:E1:BB:5D")), + Model: util.Pointer(QemuNetworkModelVirtIO), + TaggedVlans: util.Pointer(Vlans{})}}})}, + {name: `RateLimitKBps disable`, + input: map[string]interface{}{"net13": "virtio=BC:24:11:E1:BB:5D"}, + output: baseConfig(ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID13: QemuNetworkInterface{ + Connected: util.Pointer(true), + Firewall: util.Pointer(false), + MAC: util.Pointer(parseMAC("BC:24:11:E1:BB:5D")), + Model: util.Pointer(QemuNetworkModelVirtIO), + TaggedVlans: util.Pointer(Vlans{})}}})}, + {name: `RateLimitKBps 0.001`, + input: map[string]interface{}{"net14": "virtio=BC:24:11:E1:BB:5D,rate=0.001"}, + output: baseConfig(ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID14: QemuNetworkInterface{ + Connected: util.Pointer(true), + Firewall: util.Pointer(false), + MAC: util.Pointer(parseMAC("BC:24:11:E1:BB:5D")), + Model: util.Pointer(QemuNetworkModelVirtIO), + RateLimitKBps: util.Pointer(QemuNetworkRate(1)), + TaggedVlans: util.Pointer(Vlans{})}}})}, + {name: `RateLimitKBps 0.01`, + input: map[string]interface{}{"net15": "virtio=BC:24:11:E1:BB:5D,rate=0.010"}, + output: baseConfig(ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID15: QemuNetworkInterface{ + Connected: util.Pointer(true), + Firewall: util.Pointer(false), + MAC: util.Pointer(parseMAC("BC:24:11:E1:BB:5D")), + Model: util.Pointer(QemuNetworkModelVirtIO), + RateLimitKBps: util.Pointer(QemuNetworkRate(10)), + TaggedVlans: util.Pointer(Vlans{})}}})}, + {name: `RateLimitKBps 0.1`, + input: map[string]interface{}{"net16": "virtio=BC:24:11:E1:BB:5D,rate=0.1"}, + output: baseConfig(ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID16: QemuNetworkInterface{ + Connected: util.Pointer(true), + Firewall: util.Pointer(false), + MAC: util.Pointer(parseMAC("BC:24:11:E1:BB:5D")), + Model: util.Pointer(QemuNetworkModelVirtIO), + RateLimitKBps: util.Pointer(QemuNetworkRate(100)), + TaggedVlans: util.Pointer(Vlans{})}}})}, + {name: `RateLimitKBps 1`, + input: map[string]interface{}{"net17": "virtio=BC:24:11:E1:BB:5D,rate=1"}, + output: baseConfig(ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID17: QemuNetworkInterface{ + Connected: util.Pointer(true), + Firewall: util.Pointer(false), + MAC: util.Pointer(parseMAC("BC:24:11:E1:BB:5D")), + Model: util.Pointer(QemuNetworkModelVirtIO), + RateLimitKBps: util.Pointer(QemuNetworkRate(1000)), + TaggedVlans: util.Pointer(Vlans{})}}})}, + {name: `RateLimitKBps 1.264`, + input: map[string]interface{}{"net18": "virtio=BC:24:11:E1:BB:5D,rate=1.264"}, + output: baseConfig(ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID18: QemuNetworkInterface{ + Connected: util.Pointer(true), + Firewall: util.Pointer(false), + MAC: util.Pointer(parseMAC("BC:24:11:E1:BB:5D")), + Model: util.Pointer(QemuNetworkModelVirtIO), + RateLimitKBps: util.Pointer(QemuNetworkRate(1264)), + TaggedVlans: util.Pointer(Vlans{})}}})}, + {name: `RateLimitKBps 15.264`, + input: map[string]interface{}{"net19": "virtio=BC:24:11:E1:BB:5D,rate=15.264"}, + output: baseConfig(ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID19: QemuNetworkInterface{ + Connected: util.Pointer(true), + Firewall: util.Pointer(false), + MAC: util.Pointer(parseMAC("BC:24:11:E1:BB:5D")), + Model: util.Pointer(QemuNetworkModelVirtIO), + RateLimitKBps: util.Pointer(QemuNetworkRate(15264)), + TaggedVlans: util.Pointer(Vlans{})}}})}, + {name: `NaitiveVlan`, + input: map[string]interface{}{"net20": "virtio=BC:24:11:E1:BB:5D,tag=1"}, + output: baseConfig(ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID20: QemuNetworkInterface{ + Connected: util.Pointer(true), + Firewall: util.Pointer(false), + MAC: util.Pointer(parseMAC("BC:24:11:E1:BB:5D")), + Model: util.Pointer(QemuNetworkModelVirtIO), + NativeVlan: util.Pointer(Vlan(1)), + TaggedVlans: util.Pointer(Vlans{})}}})}, + {name: `TaggedVlans`, + input: map[string]interface{}{"net21": "virtio=BC:24:11:E1:BB:5D,trunks=1;63;21"}, + output: baseConfig(ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID21: QemuNetworkInterface{ + Connected: util.Pointer(true), + Firewall: util.Pointer(false), + MAC: util.Pointer(parseMAC("BC:24:11:E1:BB:5D")), + Model: util.Pointer(QemuNetworkModelVirtIO), + TaggedVlans: util.Pointer(Vlans{1, 63, 21})}}})}, + }, + }, {category: `Node`, tests: []test{ {name: `vmr nil`, @@ -6105,6 +6506,15 @@ func Test_ConfigQemu_Validate(t *testing.T) { } return config } + baseNetwork := func(id QemuNetworkInterfaceID, config QemuNetworkInterface) QemuNetworkInterfaces { + if config.Bridge == nil { + config.Bridge = util.Pointer("vmbr0") + } + if config.Model == nil { + config.Model = util.Pointer(QemuNetworkModelVirtIO) + } + return QemuNetworkInterfaces{id: config} + } validCloudInit := QemuCloudInitDisk{Format: QemuDiskFormat_Raw, Storage: "Test"} validTags := func() []Tag { array := test_data_tag.Tag_Legal() @@ -7505,6 +7915,124 @@ func Test_ConfigQemu_Validate(t *testing.T) { CapacityMiB: util.Pointer(QemuMemoryCapacity(2048)), MinimumCapacityMiB: util.Pointer(QemuMemoryBalloonCapacity(1024))}}, err: errors.New(QemuMemory_Error_SharesHasNoEffectWithoutBallooning)}}}}, + {category: `Network`, + valid: testType{ + createUpdate: []test{ + {name: `Delete`, + input: baseConfig(ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID0: QemuNetworkInterface{Delete: true}}}), + current: &ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID0: QemuNetworkInterface{}}}}, + {name: `MTU inherit model=virtio`, + input: baseConfig(ConfigQemu{Networks: baseNetwork(QemuNetworkInterfaceID0, + QemuNetworkInterface{MTU: &QemuMTU{Inherit: true}})}), + current: &ConfigQemu{Networks: QemuNetworkInterfaces{ + QemuNetworkInterfaceID0: QemuNetworkInterface{}}}}, + {name: `MTU value`, + input: baseConfig(ConfigQemu{Networks: baseNetwork(QemuNetworkInterfaceID1, + QemuNetworkInterface{ + Model: util.Pointer(QemuNetworkModelVirtIO), + MTU: &QemuMTU{Value: 1500}})}), + current: &ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID1: QemuNetworkInterface{}}}}, + {name: `MTU empty e1000`, + input: baseConfig(ConfigQemu{Networks: baseNetwork(QemuNetworkInterfaceID2, + QemuNetworkInterface{ + Model: util.Pointer(QemuNetworkModelE1000), + MTU: &QemuMTU{}})}), + current: &ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID2: QemuNetworkInterface{}}}}, + {name: `MTU empty virtio`, + input: baseConfig(ConfigQemu{Networks: baseNetwork(QemuNetworkInterfaceID2, + QemuNetworkInterface{ + Model: util.Pointer(QemuNetworkModelVirtIO), + MTU: &QemuMTU{}})}), + current: &ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID2: QemuNetworkInterface{}}}}, + {name: `Model`, + input: baseConfig(ConfigQemu{Networks: baseNetwork(QemuNetworkInterfaceID3, + QemuNetworkInterface{Model: util.Pointer(QemuNetworkModel("e1000"))})}), + current: &ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID3: QemuNetworkInterface{}}}}, + {name: `MultiQueue`, + input: baseConfig(ConfigQemu{Networks: baseNetwork(QemuNetworkInterfaceID4, + QemuNetworkInterface{MultiQueue: util.Pointer(QemuNetworkQueue(1))})}), + current: &ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID4: QemuNetworkInterface{}}}}, + {name: `RateLimitKBps`, + input: baseConfig(ConfigQemu{Networks: baseNetwork(QemuNetworkInterfaceID5, + QemuNetworkInterface{RateLimitKBps: util.Pointer(QemuNetworkRate(10240000))})}), + current: &ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID5: QemuNetworkInterface{}}}}, + {name: `NativeVlan`, + input: baseConfig(ConfigQemu{Networks: baseNetwork(QemuNetworkInterfaceID6, + QemuNetworkInterface{NativeVlan: util.Pointer(Vlan(56))})}), + current: &ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID6: QemuNetworkInterface{}}}}, + {name: `TaggedVlans`, + input: baseConfig(ConfigQemu{Networks: baseNetwork(QemuNetworkInterfaceID7, + QemuNetworkInterface{TaggedVlans: util.Pointer(Vlans{0, 4095})})}), + current: &ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID7: QemuNetworkInterface{}}}}}, + update: []test{ + {name: `MTU model change`, + input: baseConfig(ConfigQemu{Networks: baseNetwork(QemuNetworkInterfaceID0, + QemuNetworkInterface{Model: util.Pointer(QemuNetworkModelE1000)})}), + current: &ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID0: QemuNetworkInterface{ + Model: util.Pointer(QemuNetworkModelVirtIO), + MTU: &QemuMTU{Inherit: true}}}}}, + {name: `Update no Bridge && no Model`, + input: baseConfig(ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID8: QemuNetworkInterface{}}}), + current: &ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID8: QemuNetworkInterface{}}}}}}, + invalid: testType{ + create: []test{ + {name: `no Bridge`, + input: baseConfig(ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID9: QemuNetworkInterface{}}}), + err: errors.New(QemuNetworkInterface_Error_BridgeRequired)}, + {name: `no Model`, + input: baseConfig(ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID10: QemuNetworkInterface{Bridge: util.Pointer("vmbr0")}}}), + err: errors.New(QemuNetworkInterface_Error_ModelRequired)}}, + createUpdate: []test{ + {name: `errors.New(MTU_Error_Invalid)`, + input: baseConfig(ConfigQemu{Networks: baseNetwork( + QemuNetworkInterfaceID11, QemuNetworkInterface{MTU: &QemuMTU{Value: 575}})}), + current: &ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID11: QemuNetworkInterface{}}}, + err: errors.New(MTU_Error_Invalid)}, + {name: `errors.New(QemuMTU_Error_Invalid)`, + input: baseConfig(ConfigQemu{Networks: baseNetwork( + QemuNetworkInterfaceID12, QemuNetworkInterface{MTU: &QemuMTU{Inherit: true, Value: 1500}})}), + current: &ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID12: QemuNetworkInterface{}}}, + err: errors.New(QemuMTU_Error_Invalid)}, + {name: `errors.New(QemuNetworkInterface_Error_MtuNoEffect) MTU inherit`, + input: baseConfig(ConfigQemu{Networks: baseNetwork( + QemuNetworkInterfaceID12, QemuNetworkInterface{ + Model: util.Pointer(QemuNetworkModelE100082544gc), + MTU: &QemuMTU{Inherit: true}})}), + current: &ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID12: QemuNetworkInterface{}}}, + err: errors.New(QemuNetworkInterface_Error_MtuNoEffect)}, + {name: `errors.New(QemuNetworkInterface_Error_MtuNoEffect) MTU value`, + input: baseConfig(ConfigQemu{Networks: baseNetwork( + QemuNetworkInterfaceID12, QemuNetworkInterface{ + Model: util.Pointer(QemuNetworkModelE1000), + MTU: &QemuMTU{Value: 1500}})}), + current: &ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID12: QemuNetworkInterface{}}}, + err: errors.New(QemuNetworkInterface_Error_MtuNoEffect)}, + {name: `model`, + input: baseConfig(ConfigQemu{Networks: baseNetwork(QemuNetworkInterfaceID13, QemuNetworkInterface{ + Model: util.Pointer(QemuNetworkModel("invalid"))})}), + current: &ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID13: QemuNetworkInterface{}}}, + err: QemuNetworkModel("").Error()}, + {name: `errors.New(QemuNetworkQueue_Error_Invalid)`, + input: baseConfig(ConfigQemu{Networks: baseNetwork(QemuNetworkInterfaceID14, QemuNetworkInterface{ + MultiQueue: util.Pointer(QemuNetworkQueue(75))})}), + current: &ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID14: QemuNetworkInterface{}}}, + err: errors.New(QemuNetworkQueue_Error_Invalid)}, + {name: `errors.New(QemuNetworkRate_Error_Invalid)`, + input: baseConfig( + ConfigQemu{Networks: baseNetwork(QemuNetworkInterfaceID15, QemuNetworkInterface{ + RateLimitKBps: util.Pointer(QemuNetworkRate(10240001))})}), + current: &ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID15: QemuNetworkInterface{}}}, + err: errors.New(QemuNetworkRate_Error_Invalid)}, + {name: `NativeVlan errors.New(Vlan_Error_Invalid)`, + input: baseConfig(ConfigQemu{Networks: baseNetwork(QemuNetworkInterfaceID16, QemuNetworkInterface{ + NativeVlan: util.Pointer(Vlan(4096))})}), + current: &ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID16: QemuNetworkInterface{}}}, + err: errors.New(Vlan_Error_Invalid)}, + {name: `TaggedVlans errors.New(Vlan_Error_Invalid)`, + input: baseConfig(ConfigQemu{Networks: baseNetwork(QemuNetworkInterfaceID17, QemuNetworkInterface{ + TaggedVlans: util.Pointer(Vlans{4096})})}), + current: &ConfigQemu{Networks: QemuNetworkInterfaces{QemuNetworkInterfaceID17: QemuNetworkInterface{}}}, + err: errors.New(Vlan_Error_Invalid)}}}}, {category: `PoolName`, valid: testType{ createUpdate: []test{ @@ -7646,22 +8174,3 @@ func Test_ConfigQemu_Validate(t *testing.T) { } } } - -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/config_qemu_tpm.go b/proxmox/config_qemu_tpm.go index 04d1a7c7..a56ae146 100644 --- a/proxmox/config_qemu_tpm.go +++ b/proxmox/config_qemu_tpm.go @@ -33,7 +33,7 @@ func (TpmState) mapToSDK(param string) *TpmState { tmp.Storage = splitString[0] } if itemValue, isSet := setting["version"]; isSet { - tmp.Version = util.Pointer(TpmVersion(itemValue.(string))) + tmp.Version = util.Pointer(TpmVersion(itemValue)) } return &tmp diff --git a/proxmox/type_mtu.go b/proxmox/type_mtu.go new file mode 100644 index 00000000..02b7c2bf --- /dev/null +++ b/proxmox/type_mtu.go @@ -0,0 +1,14 @@ +package proxmox + +import "errors" + +type MTU uint16 // minimum value 576 - 65520 + +const MTU_Error_Invalid string = "mtu must be in the range 576-65520" + +func (mtu MTU) Validate() error { + if mtu == 0 || (mtu > 575 && mtu < 65521) { + return nil + } + return errors.New(MTU_Error_Invalid) +} diff --git a/proxmox/type_mtu_test.go b/proxmox/type_mtu_test.go new file mode 100644 index 00000000..bda431f9 --- /dev/null +++ b/proxmox/type_mtu_test.go @@ -0,0 +1,33 @@ +package proxmox + +import ( + "errors" + "testing" + + "github.com/Telmate/proxmox-api-go/test/data/test_data_mtu" + "github.com/stretchr/testify/require" +) + +func Test_MTU_Validate(t *testing.T) { + tests := []struct { + name string + input MTU + output error + }{ + {name: `MTU Valid maximum`, + input: MTU(test_data_mtu.MTU_Max_Legal())}, + {name: `MTU Valid minimum`, + input: MTU(test_data_mtu.MTU_Min_Legal())}, + {name: `MTU Invalid maximum`, + input: MTU(test_data_mtu.MTU_Max_Illegal()), + output: errors.New(MTU_Error_Invalid)}, + {name: `MTU Invalid minimum`, + input: MTU(test_data_mtu.MTU_Min_Illegal()), + output: errors.New(MTU_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/type_vlan(s).go b/proxmox/type_vlan(s).go new file mode 100644 index 00000000..6a1fa091 --- /dev/null +++ b/proxmox/type_vlan(s).go @@ -0,0 +1,54 @@ +package proxmox + +import ( + "errors" + "strconv" + "strings" +) + +type Vlan uint16 // 0-4095, 0 means no vlan + +const ( + VlanMaximum Vlan = 4095 + Vlan_Error_Invalid string = "vlan tag must be in the range 0-4095" +) + +func (config Vlan) String() string { + return strconv.FormatInt(int64(config), 10) +} + +func (config Vlan) Validate() error { + if config > VlanMaximum { + return errors.New(Vlan_Error_Invalid) + } + return nil +} + +type Vlans []Vlan + +func (config *Vlans) mapToApiUnsafe() string { + // Use a map to track seen elements and remove duplicates. + seen := make(map[Vlan]bool) + result := make([]int, 0, len(*config)) + // Iterate over the input slice and add unique elements to the result slice. + for _, value := range *config { + if _, ok := seen[value]; !ok { + seen[value] = true + result = append(result, int(value)) + } + } + builder := strings.Builder{} + for _, vlan := range result { + builder.WriteString(";" + strconv.Itoa(vlan)) + } + return builder.String() +} + +func (config Vlans) Validate() error { + for _, vlan := range config { + if err := vlan.Validate(); err != nil { + return err + } + } + return nil +} diff --git a/proxmox/type_vlan(s)_test.go b/proxmox/type_vlan(s)_test.go new file mode 100644 index 00000000..92015d13 --- /dev/null +++ b/proxmox/type_vlan(s)_test.go @@ -0,0 +1,75 @@ +package proxmox + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_Vlan_String(t *testing.T) { + tests := []struct { + name string + input Vlan + output string + }{ + {name: `0`, + input: 0, + output: "0"}, + {name: `4095`, + input: 4095, + output: "4095"}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require.Equal(t, test.output, test.input.String()) + }) + } +} + +func Test_Vlan_Validate(t *testing.T) { + tests := []struct { + name string + input Vlan + output error + }{ + {name: `Valid Maximum`, + input: 4095}, + {name: `Valid Minimum`, + input: 0}, + {name: `Invalid`, + input: 4096, + output: errors.New(Vlan_Error_Invalid), + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require.Equal(t, test.output, test.input.Validate()) + }) + } +} + +func Test_Vlans_Validate(t *testing.T) { + tests := []struct { + name string + input Vlans + output error + }{ + {name: `Valid`, + input: Vlans{0, 4095}}, + {name: `Valid Empty`, + input: Vlans{}}, + {name: `Valid Duplicate`, + input: Vlans{23, 78, 23, 90, 78}, + }, + {name: `Invalid`, + input: Vlans{0, 4095, 4096}, + output: errors.New(Vlan_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 b6b268d5..d113f976 100644 --- a/proxmox/util.go +++ b/proxmox/util.go @@ -244,9 +244,9 @@ func isIPv6(address string) bool { return strings.Count(address, ":") > 2 } -func splitStringOfSettings(settings string) map[string]interface{} { +func splitStringOfSettings(settings string) map[string]string { settingValuePairs := strings.Split(settings, ",") - settingMap := map[string]interface{}{} + settingMap := map[string]string{} for _, e := range settingValuePairs { keyValuePair := strings.SplitN(e, "=", 2) var value string diff --git a/proxmox/util_test.go b/proxmox/util_test.go index 3875a70b..ca0ef1c5 100644 --- a/proxmox/util_test.go +++ b/proxmox/util_test.go @@ -93,11 +93,11 @@ func Test_floatToTrimmedString(t *testing.T) { func Test_splitStringOfSettings(t *testing.T) { testData := []struct { Input string - Output map[string]interface{} + Output map[string]string }{ { Input: "setting=a,thing=b,randomString,doubleTest=value=equals,object=test", - Output: map[string]interface{}{ + Output: map[string]string{ "setting": "a", "thing": "b", "randomString": "", diff --git a/test/api/CloudInit/shared_test.go b/test/api/CloudInit/shared_test.go index b08e6cde..82682c8a 100644 --- a/test/api/CloudInit/shared_test.go +++ b/test/api/CloudInit/shared_test.go @@ -1,6 +1,8 @@ package api_test import ( + "net" + "github.com/Telmate/proxmox-api-go/internal/util" pxapi "github.com/Telmate/proxmox-api-go/proxmox" ) @@ -16,15 +18,7 @@ func _create_vm_spec(network bool) pxapi.ConfigQemu { disks := make(pxapi.QemuDevices) - networks := make(pxapi.QemuDevices) - if network { - networks[0] = make(map[string]interface{}) - networks[0]["bridge"] = "vmbr0" - networks[0]["firewall"] = "true" - networks[0]["id"] = "0" - networks[0]["macaddr"] = "B6:8F:9D:7C:8F:BC" - networks[0]["model"] = "virtio" - } + mac, _ := net.ParseMAC("B6:8F:9D:7C:8F:BC") config := pxapi.ConfigQemu{ Name: "test-qemu01", @@ -38,13 +32,19 @@ func _create_vm_spec(network bool) pxapi.ConfigQemu { Sockets: util.Pointer(pxapi.QemuCpuSockets(1)), Type: util.Pointer(pxapi.CpuType_QemuKvm64), }, - QemuKVM: util.Pointer(true), - Hotplug: "network,disk,usb", - QemuNetworks: networks, - QemuIso: "none", - Boot: "order=ide2;net0", - Scsihw: "virtio-scsi-pci", - QemuDisks: disks, + QemuKVM: util.Pointer(true), + Hotplug: "network,disk,usb", + + Networks: pxapi.QemuNetworkInterfaces{ + pxapi.QemuNetworkInterfaceID0: pxapi.QemuNetworkInterface{ + Bridge: util.Pointer("vmbr0"), + Firewall: util.Pointer(true), + Model: util.Pointer(pxapi.QemuNetworkModelVirtIO), + MAC: &mac}}, + QemuIso: "none", + Boot: "order=ide2;net0", + Scsihw: "virtio-scsi-pci", + QemuDisks: disks, } return config diff --git a/test/api/Qemu/shared_test.go b/test/api/Qemu/shared_test.go index 22b7d3a7..d2b82642 100644 --- a/test/api/Qemu/shared_test.go +++ b/test/api/Qemu/shared_test.go @@ -1,6 +1,8 @@ package api_test import ( + "net" + "github.com/Telmate/proxmox-api-go/internal/util" pxapi "github.com/Telmate/proxmox-api-go/proxmox" ) @@ -20,15 +22,7 @@ func _create_vm_spec(network bool) pxapi.ConfigQemu { disks[0]["storage"] = "local" disks[0]["size"] = "1G" - networks := make(pxapi.QemuDevices) - if network { - networks[0] = make(map[string]interface{}) - networks[0]["bridge"] = "vmbr0" - networks[0]["firewall"] = "true" - networks[0]["id"] = "0" - networks[0]["macaddr"] = "B6:8F:9D:7C:8F:BC" - networks[0]["model"] = "virtio" - } + mac, _ := net.ParseMAC("B6:8F:9D:7C:8F:BC") config := pxapi.ConfigQemu{ Name: "test-qemu01", @@ -42,13 +36,17 @@ func _create_vm_spec(network bool) pxapi.ConfigQemu { Sockets: util.Pointer(pxapi.QemuCpuSockets(1)), Type: util.Pointer(pxapi.CpuType_QemuKvm64), }, - QemuKVM: util.Pointer(true), - Hotplug: "network,disk,usb", - QemuNetworks: networks, - QemuIso: "none", - Boot: "order=ide2;net0", - Scsihw: "virtio-scsi-pci", - QemuDisks: disks, + QemuKVM: util.Pointer(true), + Hotplug: "network,disk,usb", + Networks: pxapi.QemuNetworkInterfaces{ + pxapi.QemuNetworkInterfaceID0: pxapi.QemuNetworkInterface{ + Bridge: util.Pointer("vmbr0"), + Firewall: util.Pointer(true), + Model: util.Pointer(pxapi.QemuNetworkModelVirtIO), + MAC: &mac}}, + QemuIso: "none", + Boot: "order=ide2;net0", + Scsihw: "virtio-scsi-pci", } return config diff --git a/test/data/test_data_mtu/type_mtu.go b/test/data/test_data_mtu/type_mtu.go new file mode 100644 index 00000000..c9a5c6b8 --- /dev/null +++ b/test/data/test_data_mtu/type_mtu.go @@ -0,0 +1,17 @@ +package test_data_mtu + +func MTU_Min_Legal() uint16 { + return 576 +} + +func MTU_Min_Illegal() uint16 { + return MTU_Min_Legal() - 1 +} + +func MTU_Max_Legal() uint16 { + return 65520 +} + +func MTU_Max_Illegal() uint16 { + return MTU_Max_Legal() + 1 +}