diff --git a/.gitignore b/.gitignore index b8715cc2..584924eb 100644 --- a/.gitignore +++ b/.gitignore @@ -3,5 +3,5 @@ proxmox-api-go .vscode .env -coverage.html -coverage.out \ No newline at end of file +*coverage.html +*coverage.out \ No newline at end of file diff --git a/proxmox/config_qemu.go b/proxmox/config_qemu.go index a599dc27..b27e7bcd 100644 --- a/proxmox/config_qemu.go +++ b/proxmox/config_qemu.go @@ -60,7 +60,7 @@ type ConfigQemu struct { 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 + USBs QemuUSBs `json:"usbs,omitempty"` 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 @@ -126,9 +126,6 @@ func (config *ConfigQemu) defaults() { if config.QemuUnusedDisks == nil { config.QemuUnusedDisks = QemuDevices{} } - if config.QemuUsbs == nil { - config.QemuUsbs = QemuDevices{} - } if config.QemuVga == nil { config.QemuVga = QemuDevice{} } @@ -268,8 +265,9 @@ func (config ConfigQemu) mapToAPI(currentConfig ConfigQemu, version Version) (re params["vga"] = strings.Join(vgaParam, ",") } - // Create usb interfaces - config.CreateQemuUsbsParams(params) + if config.USBs != nil { + itemsToDelete += config.USBs.mapToAPI(currentConfig.USBs, params) + } config.CreateQemuPCIsParams(params) @@ -431,40 +429,7 @@ func (ConfigQemu) mapToStruct(vmr *VmRef, params map[string]interface{}) (*Confi config.Networks = QemuNetworkInterfaces{}.mapToSDK(params) config.Serials = SerialInterfaces{}.mapToSDK(params) - // Add usbs - usbNames := []string{} - - for k := range params { - if usbName := rxUsbName.FindStringSubmatch(k); len(usbName) > 0 { - usbNames = append(usbNames, usbName[0]) - } - } - - if len(usbNames) > 0 { - config.QemuUsbs = QemuDevices{} - for _, usbName := range usbNames { - usbConfStr := params[usbName] - usbConfList := strings.Split(usbConfStr.(string), ",") - id := rxDeviceID.FindStringSubmatch(usbName) - usbID, _ := strconv.Atoi(id[0]) - _, host := ParseSubConf(usbConfList[0], "=") - - usbConfMap := QemuDevice{ - "id": usbID, - "host": host, - } - - usbConfMap.readDeviceConfig(usbConfList[1:]) - if usbConfMap["usb3"] == 1 { - usbConfMap["usb3"] = true - } - - // And device config to usbs map. - if len(usbConfMap) > 0 { - config.QemuUsbs[usbID] = usbConfMap - } - } - } + config.USBs = QemuUSBs{}.mapToSDK(params) // hostpci hostPCInames := []string{} @@ -714,6 +679,11 @@ func (config ConfigQemu) Validate(current *ConfigQemu, version Version) (err err return } } + if config.USBs != nil { + if err = config.USBs.Validate(nil); err != nil { + return + } + } } else { // Update if config.CPU != nil { if err = config.CPU.Validate(current.CPU, version); err != nil { @@ -735,6 +705,11 @@ func (config ConfigQemu) Validate(current *ConfigQemu, version Version) (err err return } } + if config.USBs != nil { + if err = config.USBs.Validate(current.USBs); err != nil { + return + } + } } // Shared if config.Agent != nil { @@ -826,7 +801,6 @@ var ( rxUnusedDiskName = regexp.MustCompile(`^(unused)\d+`) rxNicName = regexp.MustCompile(`net\d+`) rxMpName = regexp.MustCompile(`mp\d+`) - rxUsbName = regexp.MustCompile(`usb\d+`) rxPCIName = regexp.MustCompile(`hostpci\d+`) ) @@ -1160,16 +1134,6 @@ func (c ConfigQemu) CreateQemuPCIsParams(params map[string]interface{}) { } } -// Create parameters for usb interface -func (c ConfigQemu) CreateQemuUsbsParams(params map[string]interface{}) { - for usbID, usbConfMap := range c.QemuUsbs { - qemuUsbName := "usb" + strconv.Itoa(usbID) - - // Add back to Qemu prams. - params[qemuUsbName] = FormatUsbParam(usbConfMap) - } -} - // Create parameters for serial interface func (c ConfigQemu) CreateQemuMachineParam( params map[string]interface{}, diff --git a/proxmox/config_qemu_cloudinit.go b/proxmox/config_qemu_cloudinit.go index b69475fe..2f518876 100644 --- a/proxmox/config_qemu_cloudinit.go +++ b/proxmox/config_qemu_cloudinit.go @@ -519,13 +519,13 @@ func (CloudInitNetworkConfig) mapToSDK(param string) (config CloudInitNetworkCon } if v, isSet := params["ip6"]; isSet { ipv6Set = true - if v == "dhcp" { + switch v { + case "dhcp": ipv6.DHCP = true - } else if v == "auto" { + case "auto": ipv6.SLAAC = true - } else { - tmp := IPv6CIDR(v) - ipv6.Address = &tmp + default: + ipv6.Address = util.Pointer(IPv6CIDR(v)) } } if v, isSet := params["gw6"]; isSet { diff --git a/proxmox/config_qemu_test.go b/proxmox/config_qemu_test.go index 47f2a8b1..c39e7b4c 100644 --- a/proxmox/config_qemu_test.go +++ b/proxmox/config_qemu_test.go @@ -3757,6 +3757,168 @@ func Test_ConfigQemu_mapToAPI(t *testing.T) { {name: `TPM Delete Full`, config: &ConfigQemu{TPM: &TpmState{Storage: "test", Version: util.Pointer(TpmVersion_2_0), Delete: true}}, output: map[string]interface{}{"delete": "tpmstate0"}}}}, + {category: `USBs`, + create: []test{ + {name: `Delete`, + config: &ConfigQemu{USBs: QemuUSBs{ + QemuUsbID0: QemuUSB{Delete: true}}}, + output: map[string]interface{}{}}, + }, + createUpdate: []test{ + {name: `Device all`, + config: &ConfigQemu{USBs: QemuUSBs{ + QemuUsbID0: QemuUSB{Device: &QemuUsbDevice{ + ID: util.Pointer(UsbDeviceID("1234:5678")), + USB3: util.Pointer(true)}}}}, + currentConfig: ConfigQemu{USBs: QemuUSBs{ + QemuUsbID0: QemuUSB{Mapping: &QemuUsbMapping{}}}}, + output: map[string]interface{}{"usb0": "host=1234:5678,usb3=1"}}, + {name: `Device.USB3 false`, + config: &ConfigQemu{USBs: QemuUSBs{ + QemuUsbID1: QemuUSB{Device: &QemuUsbDevice{ + ID: util.Pointer(UsbDeviceID("abcd:35fe")), + USB3: util.Pointer(false)}}}}, + currentConfig: ConfigQemu{USBs: QemuUSBs{ + QemuUsbID1: QemuUSB{Mapping: &QemuUsbMapping{}}}}, + output: map[string]interface{}{"usb1": "host=abcd:35fe"}}, + {name: `Device.USB3 nil`, + config: &ConfigQemu{USBs: QemuUSBs{ + QemuUsbID1: QemuUSB{Device: &QemuUsbDevice{ + ID: util.Pointer(UsbDeviceID("8235:95af"))}}}}, + currentConfig: ConfigQemu{USBs: QemuUSBs{ + QemuUsbID1: QemuUSB{Mapping: &QemuUsbMapping{}}}}, + output: map[string]interface{}{"usb1": "host=8235:95af"}}, + {name: `Mapping all`, + config: &ConfigQemu{USBs: QemuUSBs{ + QemuUsbID1: QemuUSB{Mapping: &QemuUsbMapping{ + ID: util.Pointer(ResourceMappingUsbID("test")), + USB3: util.Pointer(true)}}}}, + currentConfig: ConfigQemu{USBs: QemuUSBs{ + QemuUsbID1: QemuUSB{Port: &QemuUsbPort{}}}}, + output: map[string]interface{}{"usb1": "mapping=test,usb3=1"}}, + {name: `Mapping.USB3 false`, + config: &ConfigQemu{USBs: QemuUSBs{ + QemuUsbID1: QemuUSB{Mapping: &QemuUsbMapping{ + ID: util.Pointer(ResourceMappingUsbID("test")), + USB3: util.Pointer(false)}}}}, + currentConfig: ConfigQemu{USBs: QemuUSBs{ + QemuUsbID1: QemuUSB{Port: &QemuUsbPort{}}}}, + output: map[string]interface{}{"usb1": "mapping=test"}}, + {name: `Mapping.USB3 nil`, + config: &ConfigQemu{USBs: QemuUSBs{ + QemuUsbID1: QemuUSB{Mapping: &QemuUsbMapping{ + ID: util.Pointer(ResourceMappingUsbID("test"))}}}}, + currentConfig: ConfigQemu{USBs: QemuUSBs{ + QemuUsbID1: QemuUSB{Port: &QemuUsbPort{}}}}, + output: map[string]interface{}{"usb1": "mapping=test"}}, + {name: `Port all`, + config: &ConfigQemu{USBs: QemuUSBs{ + QemuUsbID2: QemuUSB{Port: &QemuUsbPort{ + ID: util.Pointer(UsbPortID("1-2")), + USB3: util.Pointer(true)}}}}, + currentConfig: ConfigQemu{USBs: QemuUSBs{ + QemuUsbID2: QemuUSB{Spice: &QemuUsbSpice{}}}}, + output: map[string]interface{}{"usb2": "host=1-2,usb3=1"}}, + {name: `Port.USB3 false`, + config: &ConfigQemu{USBs: QemuUSBs{ + QemuUsbID2: QemuUSB{Port: &QemuUsbPort{ + ID: util.Pointer(UsbPortID("1-2")), + USB3: util.Pointer(false)}}}}, + currentConfig: ConfigQemu{USBs: QemuUSBs{ + QemuUsbID2: QemuUSB{Spice: &QemuUsbSpice{}}}}, + output: map[string]interface{}{"usb2": "host=1-2"}}, + {name: `Port.USB3 nil`, + config: &ConfigQemu{USBs: QemuUSBs{ + QemuUsbID2: QemuUSB{Port: &QemuUsbPort{ + ID: util.Pointer(UsbPortID("1-2"))}}}}, + currentConfig: ConfigQemu{USBs: QemuUSBs{ + QemuUsbID2: QemuUSB{Spice: &QemuUsbSpice{}}}}, + output: map[string]interface{}{"usb2": "host=1-2"}}, + {name: `Spice all`, + config: &ConfigQemu{USBs: QemuUSBs{ + QemuUsbID3: QemuUSB{Spice: &QemuUsbSpice{ + USB3: true}}}}, + currentConfig: ConfigQemu{USBs: QemuUSBs{ + QemuUsbID3: QemuUSB{Device: &QemuUsbDevice{}}}}, + output: map[string]interface{}{"usb3": "spice,usb3=1"}}, + {name: `Spice.USB3 false`, + config: &ConfigQemu{USBs: QemuUSBs{ + QemuUsbID3: QemuUSB{Spice: &QemuUsbSpice{ + USB3: false}}}}, + currentConfig: ConfigQemu{USBs: QemuUSBs{ + QemuUsbID3: QemuUSB{Device: &QemuUsbDevice{}}}}, + output: map[string]interface{}{"usb3": "spice"}}, + }, + update: []test{ + {name: `Delete`, + config: &ConfigQemu{USBs: QemuUSBs{ + QemuUsbID0: QemuUSB{Delete: true}}}, + currentConfig: ConfigQemu{USBs: QemuUSBs{ + QemuUsbID0: QemuUSB{Mapping: &QemuUsbMapping{}}}}, + output: map[string]interface{}{"delete": "usb0"}}, + {name: `Device.ID update`, + config: &ConfigQemu{USBs: QemuUSBs{ + QemuUsbID1: QemuUSB{Device: &QemuUsbDevice{ + ID: util.Pointer(UsbDeviceID("1234:5678"))}}}}, + currentConfig: ConfigQemu{USBs: QemuUSBs{ + QemuUsbID1: QemuUSB{Device: &QemuUsbDevice{ + ID: util.Pointer(UsbDeviceID("abcd:35fe")), + USB3: util.Pointer(true)}}}}, + output: map[string]interface{}{"usb1": "host=1234:5678,usb3=1"}}, + {name: `Device.USB3 update`, + config: &ConfigQemu{USBs: QemuUSBs{ + QemuUsbID1: QemuUSB{Device: &QemuUsbDevice{ + USB3: util.Pointer(true)}}}}, + currentConfig: ConfigQemu{USBs: QemuUSBs{ + QemuUsbID1: QemuUSB{Device: &QemuUsbDevice{ + ID: util.Pointer(UsbDeviceID("abcd:35fe")), + USB3: util.Pointer(false)}}}}, + output: map[string]interface{}{"usb1": "host=abcd:35fe,usb3=1"}}, + {name: `Mapping.ID update`, + config: &ConfigQemu{USBs: QemuUSBs{ + QemuUsbID1: QemuUSB{Mapping: &QemuUsbMapping{ + ID: util.Pointer(ResourceMappingUsbID("test"))}}}}, + currentConfig: ConfigQemu{USBs: QemuUSBs{ + QemuUsbID1: QemuUSB{Mapping: &QemuUsbMapping{ + ID: util.Pointer(ResourceMappingUsbID("test2")), + USB3: util.Pointer(true)}}}}, + output: map[string]interface{}{"usb1": "mapping=test,usb3=1"}}, + {name: `Mapping.USB3 update`, + config: &ConfigQemu{USBs: QemuUSBs{ + QemuUsbID1: QemuUSB{Mapping: &QemuUsbMapping{ + USB3: util.Pointer(true)}}}}, + currentConfig: ConfigQemu{USBs: QemuUSBs{ + QemuUsbID1: QemuUSB{Mapping: &QemuUsbMapping{ + ID: util.Pointer(ResourceMappingUsbID("test2")), + USB3: util.Pointer(false)}}}}, + output: map[string]interface{}{"usb1": "mapping=test2,usb3=1"}}, + {name: `Port.ID update`, + config: &ConfigQemu{USBs: QemuUSBs{ + QemuUsbID1: QemuUSB{Port: &QemuUsbPort{ + ID: util.Pointer(UsbPortID("1-2"))}}}}, + currentConfig: ConfigQemu{USBs: QemuUSBs{ + QemuUsbID1: QemuUSB{Port: &QemuUsbPort{ + ID: util.Pointer(UsbPortID("2-3")), + USB3: util.Pointer(true)}}}}, + output: map[string]interface{}{"usb1": "host=1-2,usb3=1"}}, + {name: `Port.USB3 update`, + config: &ConfigQemu{USBs: QemuUSBs{ + QemuUsbID1: QemuUSB{Port: &QemuUsbPort{ + USB3: util.Pointer(true)}}}}, + currentConfig: ConfigQemu{USBs: QemuUSBs{ + QemuUsbID1: QemuUSB{Port: &QemuUsbPort{ + ID: util.Pointer(UsbPortID("2-3")), + USB3: util.Pointer(false)}}}}, + output: map[string]interface{}{"usb1": "host=2-3,usb3=1"}}, + {name: `Spice`, + config: &ConfigQemu{USBs: QemuUSBs{ + QemuUsbID1: QemuUSB{Spice: &QemuUsbSpice{ + USB3: false}}}}, + currentConfig: ConfigQemu{USBs: QemuUSBs{ + QemuUsbID1: QemuUSB{Device: &QemuUsbDevice{}}}}, + output: map[string]interface{}{"usb1": "spice"}}, + }, + }, } for _, test := range tests { for _, subTest := range append(test.create, test.createUpdate...) { @@ -6404,6 +6566,57 @@ func Test_ConfigQemu_mapToStruct(t *testing.T) { {name: `All`, input: map[string]interface{}{"tpmstate0": string("local-lvm:vm-101-disk-0,size=4M,version=v2.0")}, output: baseConfig(ConfigQemu{TPM: &TpmState{Storage: "local-lvm", Version: util.Pointer(TpmVersion("v2.0"))}})}}}, + {category: `USBs`, + tests: []test{ + {name: `Device`, + input: map[string]interface{}{ + "usb0": "host=1234:5678"}, + output: baseConfig(ConfigQemu{USBs: QemuUSBs{ + QemuUsbID0: QemuUSB{Device: &QemuUsbDevice{ + ID: util.Pointer(UsbDeviceID("1234:5678")), + USB3: util.Pointer(false)}}}})}, + {name: `Device usb3`, + input: map[string]interface{}{ + "usb1": "host=1234:5678,usb3=1"}, + output: baseConfig(ConfigQemu{USBs: QemuUSBs{ + QemuUsbID1: QemuUSB{Device: &QemuUsbDevice{ + ID: util.Pointer(UsbDeviceID("1234:5678")), + USB3: util.Pointer(true)}}}})}, + {name: `Port`, + input: map[string]interface{}{"usb2": "host=1-2"}, + output: baseConfig(ConfigQemu{USBs: QemuUSBs{ + QemuUsbID2: QemuUSB{Port: &QemuUsbPort{ + ID: util.Pointer(UsbPortID("1-2")), + USB3: util.Pointer(false)}}}})}, + {name: `Port usb3`, + input: map[string]interface{}{"usb3": "host=2-4,usb3=1"}, + output: baseConfig(ConfigQemu{USBs: QemuUSBs{ + QemuUsbID3: QemuUSB{Port: &QemuUsbPort{ + ID: util.Pointer(UsbPortID("2-4")), + USB3: util.Pointer(true)}}}})}, + {name: `mapping`, + input: map[string]interface{}{"usb4": "mapping=abcde"}, + output: baseConfig(ConfigQemu{USBs: QemuUSBs{ + QemuUsbID4: QemuUSB{Mapping: &QemuUsbMapping{ + ID: util.Pointer(ResourceMappingUsbID("abcde")), + USB3: util.Pointer(false)}}}})}, + {name: `mapping usb3`, + input: map[string]interface{}{"usb0": "mapping=testmapping,usb3=1"}, + output: baseConfig(ConfigQemu{USBs: QemuUSBs{ + QemuUsbID0: QemuUSB{Mapping: &QemuUsbMapping{ + ID: util.Pointer(ResourceMappingUsbID("testmapping")), + USB3: util.Pointer(true)}}}})}, + {name: `spice`, + input: map[string]interface{}{"usb1": "spice"}, + output: baseConfig(ConfigQemu{USBs: QemuUSBs{ + QemuUsbID1: QemuUSB{Spice: &QemuUsbSpice{USB3: false}}}})}, + {name: `spice usb3`, + input: map[string]interface{}{"usb2": "spice,usb3=1"}, + output: baseConfig(ConfigQemu{USBs: QemuUSBs{ + QemuUsbID2: QemuUSB{Spice: &QemuUsbSpice{USB3: true}}}})}, + {name: `code coverage`, + input: map[string]interface{}{"usb3": ""}, + output: baseConfig(ConfigQemu{USBs: QemuUSBs{QemuUsbID3: QemuUSB{}}})}}}, {category: `VmID`, tests: []test{ {name: `vmr nil`, @@ -8139,6 +8352,154 @@ func Test_ConfigQemu_Validate(t *testing.T) { input: baseConfig(ConfigQemu{TPM: &TpmState{Storage: "test", Version: util.Pointer(TpmVersion(""))}}), current: &ConfigQemu{TPM: &TpmState{}}, err: errors.New(TpmVersion_Error_Invalid)}}}}, + {category: `USBs`, + valid: testType{ + createUpdate: []test{ + {name: `delete`, + input: baseConfig(ConfigQemu{USBs: QemuUSBs{ + QemuUsbID0: QemuUSB{Delete: true}}}), + current: &ConfigQemu{USBs: QemuUSBs{ + QemuUsbID0: QemuUSB{Device: &QemuUsbDevice{ + ID: util.Pointer(UsbDeviceID("1234:5678"))}}}}}, + {name: `USBs.Device.ID set/update`, + input: baseConfig(ConfigQemu{USBs: QemuUSBs{ + QemuUsbID0: QemuUSB{Device: &QemuUsbDevice{ + ID: util.Pointer(UsbDeviceID("5678:1234"))}}}}), + current: &ConfigQemu{USBs: QemuUSBs{ + QemuUsbID0: QemuUSB{Device: &QemuUsbDevice{ + ID: util.Pointer(UsbDeviceID("1234:5678")), + USB3: util.Pointer(true)}}}}}, + {name: `USBs.Mapped.ID set/update`, + input: baseConfig(ConfigQemu{USBs: QemuUSBs{ + QemuUsbID0: QemuUSB{Mapping: &QemuUsbMapping{ + ID: util.Pointer(ResourceMappingUsbID("valid"))}}}}), + current: &ConfigQemu{USBs: QemuUSBs{ + QemuUsbID0: QemuUSB{Mapping: &QemuUsbMapping{ + ID: util.Pointer(ResourceMappingUsbID("test")), + USB3: util.Pointer(true)}}}}}, + {name: `USBs.Port.ID set/update`, + input: baseConfig(ConfigQemu{USBs: QemuUSBs{ + QemuUsbID0: QemuUSB{Port: &QemuUsbPort{ + ID: util.Pointer(UsbPortID("6-4"))}}}}), + current: &ConfigQemu{USBs: QemuUSBs{ + QemuUsbID0: QemuUSB{Port: &QemuUsbPort{ + ID: util.Pointer(UsbPortID("1-5")), + USB3: util.Pointer(true)}}}}}, + {name: `USBs.Spice.USB3 set/update`, + input: baseConfig(ConfigQemu{USBs: QemuUSBs{ + QemuUsbID0: QemuUSB{Spice: &QemuUsbSpice{ + USB3: true}}}}), + current: &ConfigQemu{USBs: QemuUSBs{ + QemuUsbID0: QemuUSB{Spice: &QemuUsbSpice{ + USB3: false}}}}}}, + update: []test{ + {name: `USBs.Device to USBs.Mapped`, + input: baseConfig(ConfigQemu{USBs: QemuUSBs{ + QemuUsbID0: QemuUSB{Device: &QemuUsbDevice{ + ID: util.Pointer(UsbDeviceID("1234:5678"))}}}}), + current: &ConfigQemu{USBs: QemuUSBs{ + QemuUsbID0: QemuUSB{Mapping: &QemuUsbMapping{ + ID: util.Pointer(ResourceMappingUsbID("test"))}}}}}, + {name: `USBs.Device.USB3 update`, + input: baseConfig(ConfigQemu{USBs: QemuUSBs{ + QemuUsbID0: QemuUSB{Device: &QemuUsbDevice{ + USB3: util.Pointer(true)}}}}), + current: &ConfigQemu{USBs: QemuUSBs{ + QemuUsbID0: QemuUSB{Device: &QemuUsbDevice{ + ID: util.Pointer(UsbDeviceID("1234:5678")), + USB3: util.Pointer(false)}}}}}, + {name: `USBs.Mapped to USBs.Port`, + input: baseConfig(ConfigQemu{USBs: QemuUSBs{ + QemuUsbID0: QemuUSB{Mapping: &QemuUsbMapping{ + ID: util.Pointer(ResourceMappingUsbID("test"))}}}}), + current: &ConfigQemu{USBs: QemuUSBs{ + QemuUsbID0: QemuUSB{Port: &QemuUsbPort{ + ID: util.Pointer(UsbPortID("3-5"))}}}}}, + {name: `USBs.Mapped.USB3 update`, + input: baseConfig(ConfigQemu{USBs: QemuUSBs{ + QemuUsbID0: QemuUSB{Mapping: &QemuUsbMapping{ + USB3: util.Pointer(true)}}}}), + current: &ConfigQemu{USBs: QemuUSBs{ + QemuUsbID0: QemuUSB{Mapping: &QemuUsbMapping{ + ID: util.Pointer(ResourceMappingUsbID("test")), + USB3: util.Pointer(false)}}}}}, + {name: `USBs.Port to USBs.Spice`, + input: baseConfig(ConfigQemu{USBs: QemuUSBs{ + QemuUsbID0: QemuUSB{Port: &QemuUsbPort{ + ID: util.Pointer(UsbPortID("2-6"))}}}}), + current: &ConfigQemu{USBs: QemuUSBs{ + QemuUsbID0: QemuUSB{Spice: &QemuUsbSpice{}}}}}, + {name: `USBs.Port.USB3 update`, + input: baseConfig(ConfigQemu{USBs: QemuUSBs{ + QemuUsbID0: QemuUSB{Port: &QemuUsbPort{ + USB3: util.Pointer(true)}}}}), + current: &ConfigQemu{USBs: QemuUSBs{ + QemuUsbID0: QemuUSB{Port: &QemuUsbPort{ + ID: util.Pointer(UsbPortID("2-6")), + USB3: util.Pointer(false)}}}}}, + {name: `USBs.Spice to USBs.Device`, + input: baseConfig(ConfigQemu{USBs: QemuUSBs{ + QemuUsbID0: QemuUSB{Spice: &QemuUsbSpice{}}}}), + current: &ConfigQemu{USBs: QemuUSBs{ + QemuUsbID0: QemuUSB{Device: &QemuUsbDevice{ + ID: util.Pointer(UsbDeviceID("5678:1234"))}}}}}}}, + invalid: testType{ + create: []test{}, + createUpdate: []test{ + {name: `errors.New(QemuUsbID_Error_Invalid)`, + input: baseConfig(ConfigQemu{USBs: QemuUSBs{ + 20: QemuUSB{}}}), + current: &ConfigQemu{USBs: QemuUSBs{ + QemuUsbID0: QemuUSB{Device: &QemuUsbDevice{}}}}, + err: errors.New(QemuUsbID_Error_Invalid)}, + {name: `errors.New(QemuUSB_Error_MutualExclusive)`, + input: baseConfig(ConfigQemu{USBs: QemuUSBs{ + QemuUsbID0: QemuUSB{ + Device: &QemuUsbDevice{ID: util.Pointer(UsbDeviceID("1234:5678"))}, + Mapping: &QemuUsbMapping{ID: util.Pointer(ResourceMappingUsbID("test"))}}}}), + current: &ConfigQemu{USBs: QemuUSBs{ + QemuUsbID0: QemuUSB{ + Device: &QemuUsbDevice{ID: util.Pointer(UsbDeviceID("1234:5678"))}}}}, + err: errors.New(QemuUSB_Error_MutualExclusive)}, + {name: `errors.New(QemuUSB_Error_DeviceID)`, + input: baseConfig(ConfigQemu{USBs: QemuUSBs{ + QemuUsbID0: QemuUSB{Device: &QemuUsbDevice{}}}}), + current: &ConfigQemu{USBs: QemuUSBs{ + QemuUsbID0: QemuUSB{Mapping: &QemuUsbMapping{}}}}, + err: errors.New(QemuUSB_Error_DeviceID)}, + {name: `errors.New(QemuUSB_Error_MappedID)`, + input: baseConfig(ConfigQemu{USBs: QemuUSBs{ + QemuUsbID0: QemuUSB{Mapping: &QemuUsbMapping{}}}}), + current: &ConfigQemu{USBs: QemuUSBs{ + QemuUsbID0: QemuUSB{Port: &QemuUsbPort{}}}}, + err: errors.New(QemuUSB_Error_MappedID)}, + {name: `errors.New(QemuUSB_Error_PortID)`, + input: baseConfig(ConfigQemu{USBs: QemuUSBs{ + QemuUsbID0: QemuUSB{Port: &QemuUsbPort{}}}}), + current: &ConfigQemu{USBs: QemuUSBs{ + QemuUsbID0: QemuUSB{Device: &QemuUsbDevice{}}}}, + err: errors.New(QemuUSB_Error_PortID)}, + {name: `errors.New(UsbDeviceID_Error_Invalid)`, + input: baseConfig(ConfigQemu{USBs: QemuUSBs{ + QemuUsbID0: QemuUSB{Device: &QemuUsbDevice{ + ID: util.Pointer(UsbDeviceID("1234"))}}}}), + current: &ConfigQemu{USBs: QemuUSBs{ + QemuUsbID0: QemuUSB{Device: &QemuUsbDevice{}}}}, + err: errors.New(UsbDeviceID_Error_Invalid)}, + {name: `errors.New(ResourceMappingUsbID_Error_Invalid)`, + input: baseConfig(ConfigQemu{USBs: QemuUSBs{ + QemuUsbID0: QemuUSB{Mapping: &QemuUsbMapping{ + ID: util.Pointer(ResourceMappingUsbID("Invalid%"))}}}}), + current: &ConfigQemu{USBs: QemuUSBs{ + QemuUsbID0: QemuUSB{Mapping: &QemuUsbMapping{}}}}, + err: errors.New(ResourceMappingUsbID_Error_Invalid)}, + {name: `errors.New(UsbPortID_Error_Invalid)`, + input: baseConfig(ConfigQemu{USBs: QemuUSBs{ + QemuUsbID0: QemuUSB{Port: &QemuUsbPort{ + ID: util.Pointer(UsbPortID("2-3-4"))}}}}), + current: &ConfigQemu{USBs: QemuUSBs{ + QemuUsbID0: QemuUSB{Port: &QemuUsbPort{}}}}, + err: errors.New(UsbPortID_Error_Invalid)}}}}, } for _, test := range tests { for _, subTest := range append(test.valid.create, test.valid.createUpdate...) { diff --git a/proxmox/config_qemu_usb.go b/proxmox/config_qemu_usb.go new file mode 100644 index 00000000..6aca7c54 --- /dev/null +++ b/proxmox/config_qemu_usb.go @@ -0,0 +1,417 @@ +package proxmox + +import ( + "errors" + "strconv" + "strings" + + "github.com/Telmate/proxmox-api-go/internal/util" +) + +type qemuUSB struct { + Type qemuUsbType + Host string + Usb3 bool + Mapping ResourceMappingUsbID +} + +func (usb qemuUSB) String() (param string) { + switch usb.Type { + case qemuUsbTypeSpice: + param = "spice" + case qemuUsbTypeMapping: + param = "mapping=" + usb.Mapping.String() + case qemuUsbTypeDevice: + param = "host=" + usb.Host + case qemuUsbTypePort: + param = "host=" + usb.Host + } + if usb.Usb3 { + param += ",usb3=1" + } + return +} + +type qemuUsbType uint8 + +const ( + qemuUsbTypeSpice qemuUsbType = 0 + qemuUsbTypeMapping qemuUsbType = 1 + qemuUsbTypeDevice qemuUsbType = 2 + qemuUsbTypePort qemuUsbType = 3 +) + +type QemuUSBs map[QemuUsbID]QemuUSB + +const QemuUSBsAmount = uint8(QemuUsbIDMaximum) + 1 + +func (QemuUSBs) mapToSDK(params map[string]interface{}) QemuUSBs { + usbList := make(QemuUSBs) + for i := QemuUsbID(0); i < 14; i++ { + if v, isSet := params["usb"+strconv.Itoa(int(i))]; isSet { + usbList[i] = QemuUSB{}.mapToSDK(v.(string)) + } + } + if len(usbList) > 0 { + return usbList + } + return nil +} + +func (config QemuUSBs) mapToAPI(current QemuUSBs, params map[string]interface{}) string { + var builder strings.Builder + for i, e := range config { + if v, isSet := current[i]; isSet { + if e.Delete { + builder.WriteString(",usb" + strconv.Itoa(int(i))) + continue + } + params["usb"+strconv.Itoa(int(i))] = e.mapToAPI(&v) + } else { + if e.Delete { + continue + } + params["usb"+strconv.Itoa(int(i))] = e.mapToAPI(nil) + } + } + return builder.String() +} + +func (config QemuUSBs) Validate(current QemuUSBs) (err error) { + for i, e := range config { + if err = i.Validate(); err != nil { + return + } + if e.Delete { + continue + } + if current != nil { + if v, isSet := (current)[i]; isSet { + if err = e.Validate(&v); err != nil { + return + } + } + } else { + if err = e.Validate(nil); err != nil { + return + } + } + } + return nil +} + +type QemuUsbID uint8 + +const ( + QemuUsbID_Error_Invalid string = "usb id must be in the range 0-4" + + QemuUsbIDMaximum = QemuUsbID4 + + QemuUsbID0 QemuUsbID = 0 + QemuUsbID1 QemuUsbID = 1 + QemuUsbID2 QemuUsbID = 2 + QemuUsbID3 QemuUsbID = 3 + QemuUsbID4 QemuUsbID = 4 +) + +func (id QemuUsbID) Validate() error { + if id > QemuUsbIDMaximum { + return errors.New(QemuUsbID_Error_Invalid) + } + return nil +} + +type QemuUSB struct { + Delete bool `json:"delete,omitempty"` + Device *QemuUsbDevice `json:"device,omitempty"` + Mapping *QemuUsbMapping `json:"mapping,omitempty"` + Port *QemuUsbPort `json:"port,omitempty"` + Spice *QemuUsbSpice `json:"spice,omitempty"` +} + +const ( + QemuUSB_Error_MutualExclusive string = "usb device, usb mapped, usb port, and usb spice are mutually exclusive" + QemuUSB_Error_DeviceID string = "usb device id is required during creation" + QemuUSB_Error_MappedID string = "usb mapped id is required during creation" + QemuUSB_Error_PortID string = "usb port id is required during creation" +) + +func (config QemuUSB) mapToAPI(current *QemuUSB) string { + var usb qemuUSB + if current != nil { + if current.Device != nil { + if current.Device.ID != nil { + usb.Host = (*current.Device.ID).String() + } + if current.Device.USB3 != nil { + usb.Usb3 = *current.Device.USB3 + } + } else if current.Mapping != nil { + if current.Mapping.ID != nil { + usb.Mapping = *current.Mapping.ID + } + if current.Mapping.USB3 != nil { + usb.Usb3 = *current.Mapping.USB3 + } + } else if current.Port != nil { + if current.Port.ID != nil { + usb.Host = string(*current.Port.ID) + } + if current.Port.USB3 != nil { + usb.Usb3 = *current.Port.USB3 + } + } else if current.Spice != nil { + usb.Usb3 = current.Spice.USB3 + } + } + if config.Device != nil { + usb.Type = qemuUsbTypeDevice + if config.Device.USB3 != nil { + usb.Usb3 = *config.Device.USB3 + } + if config.Device.ID != nil { + usb.Host = (*config.Device.ID).String() + } + } else if config.Mapping != nil { + usb.Type = qemuUsbTypeMapping + if config.Mapping.USB3 != nil { + usb.Usb3 = *config.Mapping.USB3 + } + if config.Mapping.ID != nil { + usb.Mapping = *config.Mapping.ID + } + } else if config.Port != nil { + usb.Type = qemuUsbTypePort + if config.Port.USB3 != nil { + usb.Usb3 = *config.Port.USB3 + } + if config.Port.ID != nil { + usb.Host = (*config.Port.ID).String() + } + } else if config.Spice != nil { + usb.Type = qemuUsbTypeSpice + if config.Spice.USB3 { + usb.Usb3 = config.Spice.USB3 + } + } + return usb.String() +} + +func (QemuUSB) mapToSDK(rawUSB string) QemuUSB { + var usb3 bool + splitUSB := strings.Split(rawUSB, ",") + if len(splitUSB) == 2 { + usb3 = splitUSB[1] == "usb3=1" + } + usbType := strings.Split(splitUSB[0], "=") + switch usbType[0] { + case "host": + if strings.Contains(usbType[1], ":") { + return QemuUSB{Device: &QemuUsbDevice{ID: util.Pointer(UsbDeviceID(usbType[1])), USB3: &usb3}} + } + return QemuUSB{Port: &QemuUsbPort{ID: util.Pointer(UsbPortID(usbType[1])), USB3: &usb3}} + case "mapping": + return QemuUSB{Mapping: &QemuUsbMapping{ID: util.Pointer(ResourceMappingUsbID(usbType[1])), USB3: &usb3}} + case "spice": + return QemuUSB{Spice: &QemuUsbSpice{USB3: usb3}} + } + return QemuUSB{} +} + +func (config QemuUSB) Validate(current *QemuUSB) error { + if config.Delete { + return nil + } + var usb QemuUSB + if current != nil { + if current.Device != nil { + usb.Device = current.Device + } + if current.Mapping != nil { + usb.Mapping = current.Mapping + } + if current.Port != nil { + usb.Port = current.Port + } + if current.Spice != nil { + usb.Spice = current.Spice + } + } + var mutualExclusivity uint8 + if config.Device != nil { + var tmpUSB QemuUsbDevice + if config.Device.ID != nil { + if err := config.Device.ID.Validate(); err != nil { + return err + } + tmpUSB.ID = config.Device.ID + } + if config.Device.USB3 != nil { + tmpUSB.USB3 = config.Device.USB3 + } + if usb.Device != nil { + if tmpUSB.ID != nil { + usb.Device.ID = tmpUSB.ID + } + if tmpUSB.USB3 != nil { + usb.Device.USB3 = tmpUSB.USB3 + } + } else { + usb.Device = config.Device + } + if usb.Device.ID == nil { + return errors.New(QemuUSB_Error_DeviceID) + } + mutualExclusivity++ + } + if config.Mapping != nil { + var tmpUSB QemuUsbMapping + if config.Mapping.ID != nil { + if err := config.Mapping.ID.Validate(); err != nil { + return err + } + tmpUSB.ID = config.Mapping.ID + } + if config.Mapping.USB3 != nil { + tmpUSB.USB3 = config.Mapping.USB3 + } + if usb.Mapping != nil { + if tmpUSB.ID != nil { + usb.Mapping.ID = tmpUSB.ID + } + if tmpUSB.USB3 != nil { + usb.Mapping.USB3 = tmpUSB.USB3 + } + } else { + usb.Mapping = config.Mapping + } + if usb.Mapping.ID == nil { + return errors.New(QemuUSB_Error_MappedID) + } + mutualExclusivity++ + } + if config.Port != nil { + var tmpUSB QemuUsbPort + if config.Port.ID != nil { + if err := config.Port.ID.Validate(); err != nil { + return err + } + tmpUSB.ID = config.Port.ID + } + if config.Port.USB3 != nil { + tmpUSB.USB3 = config.Port.USB3 + } + if usb.Port != nil { + if tmpUSB.ID != nil { + usb.Port.ID = tmpUSB.ID + } + if tmpUSB.USB3 != nil { + usb.Port.USB3 = tmpUSB.USB3 + } + } else { + usb.Port = config.Port + } + if usb.Port.ID == nil { + return errors.New(QemuUSB_Error_PortID) + } + mutualExclusivity++ + } + if config.Spice != nil { + mutualExclusivity++ + usb.Spice = config.Spice + } + if mutualExclusivity > 1 { + return errors.New(QemuUSB_Error_MutualExclusive) + } + return nil +} + +type QemuUsbDevice struct { + ID *UsbDeviceID `json:"id,omitempty"` + USB3 *bool `json:"usb3,omitempty"` +} + +func (config QemuUsbDevice) Validate() error { + if config.ID == nil { + return nil + } + return config.ID.Validate() +} + +type QemuUsbMapping struct { + ID *ResourceMappingUsbID `json:"id,omitempty"` + USB3 *bool `json:"usb3,omitempty"` +} + +func (config QemuUsbMapping) Validate() error { + if config.ID == nil { + return nil + } + return config.ID.Validate() +} + +type QemuUsbPort struct { + ID *UsbPortID `json:"id,omitempty"` + USB3 *bool `json:"usb3,omitempty"` +} + +func (config QemuUsbPort) Validate() error { + if config.ID == nil { + return nil + } + return config.ID.Validate() +} + +type QemuUsbSpice struct { + USB3 bool `json:"usb3"` +} + +type UsbDeviceID string + +const ( + UsbDeviceID_Error_Invalid string = "invalid usb device-id" + UsbDeviceID_Error_VendorID string = "usb vendor-id isn't valid hexadecimal" + UsbDeviceID_Error_ProductID string = "usb product-id isn't valid hexadecimal" +) + +func (id UsbDeviceID) Validate() error { + rawID := strings.Split(string(id), ":") + if len(rawID) != 2 { + return errors.New(UsbDeviceID_Error_Invalid) + } + if _, err := strconv.ParseUint(rawID[0], 16, 16); err != nil { + return errors.New(UsbDeviceID_Error_VendorID) + } + if _, err := strconv.ParseUint(rawID[1], 16, 16); err != nil { + return errors.New(UsbDeviceID_Error_ProductID) + } + return nil +} + +func (id UsbDeviceID) String() string { + return string(id) +} + +type UsbPortID string // regex: \d+-\d+ + +const ( + UsbPortID_Error_Invalid string = "invalid usb port id" +) + +func (id UsbPortID) String() string { + return string(id) +} + +func (id UsbPortID) Validate() error { + idArray := strings.Split(string(id), "-") + if len(idArray) != 2 { + return errors.New(UsbPortID_Error_Invalid) + } + if _, err := strconv.Atoi(idArray[0]); err != nil { + return errors.New(UsbPortID_Error_Invalid) + } + if _, err := strconv.Atoi(idArray[1]); err != nil { + return errors.New(UsbPortID_Error_Invalid) + } + return nil +} diff --git a/proxmox/config_qemu_usb_test.go b/proxmox/config_qemu_usb_test.go new file mode 100644 index 00000000..f51cd129 --- /dev/null +++ b/proxmox/config_qemu_usb_test.go @@ -0,0 +1,191 @@ +package proxmox + +import ( + "errors" + "testing" + + "github.com/Telmate/proxmox-api-go/internal/util" + "github.com/Telmate/proxmox-api-go/test/data/test_data_resourcemapping" + "github.com/stretchr/testify/require" +) + +func Test_QemuUsbID_Validate(t *testing.T) { + tests := []struct { + name string + input QemuUsbID + output error + }{ + {name: "Valid", + input: 0}, + {name: "Valid max", + input: 4}, + // Invalid + {name: "QemuUsbID_Error_Invalid", + input: 5, + output: errors.New(QemuUsbID_Error_Invalid)}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require.Equal(t, test.output, test.input.Validate()) + }) + } +} + +func Test_QemuUSB_Validate(t *testing.T) { + type testInput struct { + config QemuUSB + current *QemuUSB + } + tests := []struct { + name string + input testInput + output error + }{ + {name: "Valid delete", + input: testInput{config: QemuUSB{Delete: true}}}, + } + 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_QemuUsbDevice_Validate(t *testing.T) { + tests := []struct { + name string + input QemuUsbDevice + output error + }{ + {name: "Valid set", + input: QemuUsbDevice{ + ID: util.Pointer(UsbDeviceID("1234:5678"))}}, + {name: "Valid nil"}, + // Invalid + {name: "UsbDeviceID_Error_Invalid", + input: QemuUsbDevice{ + ID: util.Pointer(UsbDeviceID("162E"))}, + output: errors.New(UsbDeviceID_Error_Invalid)}, + {name: "UsbDeviceID_Error_VendorID", + input: QemuUsbDevice{ + ID: util.Pointer(UsbDeviceID("7P03:162E"))}, + output: errors.New(UsbDeviceID_Error_VendorID)}, + {name: "UsbDeviceID_Error_ProductID", + input: QemuUsbDevice{ + ID: util.Pointer(UsbDeviceID("162e:7P03"))}, + output: errors.New(UsbDeviceID_Error_ProductID)}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require.Equal(t, test.output, test.input.Validate()) + }) + } +} + +func Test_QemuUsbMapped_Validate(t *testing.T) { + tests := []struct { + name string + input QemuUsbMapping + output error + }{ + {name: "Valid set", + input: QemuUsbMapping{ + ID: util.Pointer(ResourceMappingUsbID(test_data_resourcemapping.ResourceMappingUsbID_Legal()[0]))}}, + {name: "Valid nil"}, + // Invalid + {name: "ResourceMappingUsbID_Error_MinLength", + input: QemuUsbMapping{ + ID: util.Pointer(ResourceMappingUsbID(test_data_resourcemapping.ResourceMappingUsbID_Min_Illegal()[0]))}, + output: errors.New(ResourceMappingUsbID_Error_MinLength)}, + {name: "ResourceMappingUsbID_Error_MaxLength", + input: QemuUsbMapping{ + ID: util.Pointer(ResourceMappingUsbID(test_data_resourcemapping.ResourceMappingUsbID_Max_Illegal()))}, + output: errors.New(ResourceMappingUsbID_Error_MaxLength)}, + {name: "ResourceMappingUsbID_Error_Start", + input: QemuUsbMapping{ + ID: util.Pointer(ResourceMappingUsbID(test_data_resourcemapping.ResourceMappingUsbID_Start_Illegal()[0]))}, + output: errors.New(ResourceMappingUsbID_Error_Start)}, + {name: "ResourceMappingUsbID_Error_Invalid", + input: QemuUsbMapping{ + ID: util.Pointer(ResourceMappingUsbID(test_data_resourcemapping.ResourceMappingUsbID_Character_Illegal()[0]))}, + output: errors.New(ResourceMappingUsbID_Error_Invalid)}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require.Equal(t, test.output, test.input.Validate()) + }) + } +} + +func Test_QemuUsbPort_Validate(t *testing.T) { + tests := []struct { + name string + input QemuUsbPort + output error + }{ + {name: "Valid set", + input: QemuUsbPort{ID: util.Pointer(UsbPortID("1-3"))}}, + {name: "Valid nil"}, + // Invalid + {name: "UsbDeviceID_Error_Invalid", + input: QemuUsbPort{ID: util.Pointer(UsbPortID("2-4-5"))}, + output: errors.New(UsbPortID_Error_Invalid)}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require.Equal(t, test.output, test.input.Validate()) + }) + } +} + +func Test_UsbDeviceID_Validate(t *testing.T) { + tests := []struct { + name string + input UsbDeviceID + output error + }{ + {name: "Valid", + input: "1234:5678"}, + // Invalid + {name: "UsbDeviceID_Error_Invalid", + input: "162E", + output: errors.New(UsbDeviceID_Error_Invalid)}, + {name: "UsbDeviceID_Error_VendorID", + input: "7P03:162E", + output: errors.New(UsbDeviceID_Error_VendorID)}, + {name: "UsbDeviceID_Error_ProductID", + input: "162e:7P03", + output: errors.New(UsbDeviceID_Error_ProductID)}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require.Equal(t, test.output, test.input.Validate()) + }) + } +} + +func Test_UsbPortID_Validate(t *testing.T) { + tests := []struct { + name string + input UsbPortID + output error + }{ + {name: "Valid", + input: "2-4"}, + // Invalid + {name: "UsbPortID_Error_Invalid", + input: "2-4-5", + output: errors.New(UsbPortID_Error_Invalid)}, + {name: "UsbPortID_Error_Invalid", + input: "a-2", + output: errors.New(UsbPortID_Error_Invalid)}, + {name: "UsbPortID_Error_Invalid", + input: "2-b", + output: errors.New(UsbPortID_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/resource_mapping.go b/proxmox/resource_mapping.go new file mode 100644 index 00000000..9abd83ac --- /dev/null +++ b/proxmox/resource_mapping.go @@ -0,0 +1,41 @@ +package proxmox + +import ( + "errors" + "regexp" + "unicode" +) + +// minimum length: 2 +// ,maximum length: 128 +// ,regex: ^\w(\w|\d|_|-){1,127}$ +type ResourceMappingUsbID string + +var resourceMappingUsbID = regexp.MustCompile(`^(\w|\d|_|-)+$`) + +const ( + ResourceMappingUsbID_Error_MaxLength string = "usb id is too long" + ResourceMappingUsbID_Error_MinLength string = "usb id is too short" + ResourceMappingUsbID_Error_Start string = "usb id must start with a letter" + ResourceMappingUsbID_Error_Invalid string = "usb id should match the following regex: '^\\w(\\w|\\d|_|-){1,127}$'" +) + +func (id ResourceMappingUsbID) String() string { + return string(id) +} + +func (id ResourceMappingUsbID) Validate() error { + if len(id) < 2 { + return errors.New(ResourceMappingUsbID_Error_MinLength) + } + if len(id) > 128 { + return errors.New(ResourceMappingUsbID_Error_MaxLength) + } + if !unicode.IsLetter(rune(id[0])) { + return errors.New(ResourceMappingUsbID_Error_Start) + } + if !resourceMappingUsbID.MatchString(string(id)) { + return errors.New(ResourceMappingUsbID_Error_Invalid) + } + return nil +} diff --git a/proxmox/resource_mapping_test.go b/proxmox/resource_mapping_test.go new file mode 100644 index 00000000..181ad1d2 --- /dev/null +++ b/proxmox/resource_mapping_test.go @@ -0,0 +1,40 @@ +package proxmox + +import ( + "errors" + "testing" + + "github.com/Telmate/proxmox-api-go/test/data/test_data_resourcemapping" + "github.com/stretchr/testify/require" +) + +func Test_ResourceMappingUsbID_Validate(t *testing.T) { + tests := []struct { + name string + input []string + err error + }{ + // Valid + {name: "Valid", input: test_data_resourcemapping.ResourceMappingUsbID_Legal()}, + // Invalid + {name: "Invalid ResourceMappingUsbID_Error_MinLength", + input: test_data_resourcemapping.ResourceMappingUsbID_Min_Illegal(), + err: errors.New(ResourceMappingUsbID_Error_MinLength)}, + {name: "Invalid ResourceMappingUsbID_Error_MaxLength", + input: []string{test_data_resourcemapping.ResourceMappingUsbID_Max_Illegal()}, + err: errors.New(ResourceMappingUsbID_Error_MaxLength)}, + {name: "Invalid ResourceMappingUsbID_Error_Start", + input: test_data_resourcemapping.ResourceMappingUsbID_Start_Illegal(), + err: errors.New(ResourceMappingUsbID_Error_Start)}, + {name: "Invalid ResourceMappingUsbID_Error_Invalid", + input: test_data_resourcemapping.ResourceMappingUsbID_Character_Illegal(), + err: errors.New(ResourceMappingUsbID_Error_Invalid)}, + } + for _, test := range tests { + for _, snapshot := range test.input { + t.Run(test.name+" :"+snapshot, func(*testing.T) { + require.Equal(t, ResourceMappingUsbID(snapshot).Validate(), test.err, test.name+" :"+snapshot) + }) + } + } +} diff --git a/test/data/test_data_resourcemapping/type_ResourceMappingUsbID.go b/test/data/test_data_resourcemapping/type_ResourceMappingUsbID.go new file mode 100644 index 00000000..3e9dbf66 --- /dev/null +++ b/test/data/test_data_resourcemapping/type_ResourceMappingUsbID.go @@ -0,0 +1,131 @@ +package test_data_resourcemapping + +// illegal character +func ResourceMappingUsbID_Character_Illegal() []string { + return []string{ + "aBc123!4567890_-", + "Qwer@ty-1234_ABCDEFGHIJKLMNOPQRSTUVWXYZ", + "x1y2#z3_4-5-6-7-8-9", + "HelloWo$rld_2023", + "Ab1_%cd2_ef3-gh4-ij5", + "a-_-^_-_-_-_-_-_-_-_-_-", + "snaps&hotName_2433242", + "A1_B2-*C3_D4-E5_F6", + "Xyz-123(_456_789-0", + "Test_Cas)e-123_456_789_0", + "a_1+", + "B-c_=2-D", + "E3_f4-G5_:H6-I7", + "JKL_MNO_PQ;R-STU_VWX_YZ0", + "aBgnhfjkfgd'ihfghudsfgio", + `Cdsdjfidshfu"isdghfsgffghdsufsdhfgdsfuah`, + "Ef-`gh", + "Ij-k~l-mn", + "Op-qr-st-u-vw-xy-z0-12-34-56-[78-90", + "Abcd_1234-EFGH_]5678-IJKL_9012", + "M-n-Op-qR-sT-uV{-wX-yZ", + "a_b-c_d_e-f_g_h_}i_j_k_l_m_n-o-p-q-r-s-t", + "Aa1_Bb2-C,c3_Dd4-Ee5_Ff6-Gg7_Hh8-Ii9", + "JjKkLl-MmNnOo.PpQqRrSsTtUuVvWwXxYyZz01", + "A->1", + "B-2<_C-3", + "D-4_?E-5-F-6", + "G-7-H/-8-I-9", + `J-0_K-\1-L-2-M-3-N-4-O-5-P-6-Q-7-R-8-S-9`, + "T-0_U-1-|V-2-W-3-X-4-Y-5-Z-6-7-8-9-0", + "a2😀", + } +} + +// 128 valid characters +func ResourceMappingUsbID_Max_Legal() string { + return "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-" +} + +// 129 invalid characters +func ResourceMappingUsbID_Max_Illegal() string { + return ResourceMappingUsbID_Max_Legal() + "A" +} + +// 3 valid characters +func ResourceMappingUsbID_Min_Legal() string { + return "abc" +} + +// 2 invalid characters +func ResourceMappingUsbID_Min_Illegal() []string { + return []string{"a", ""} +} + +// legal starting character +func ResourceMappingUsbID_Start_Legal() string { + return "abc" +} + +// illegal starting character +func ResourceMappingUsbID_Start_Illegal() []string { + return []string{ + "_" + ResourceMappingUsbID_Start_Legal(), + "-" + ResourceMappingUsbID_Start_Legal(), + "0" + ResourceMappingUsbID_Start_Legal(), + "5" + ResourceMappingUsbID_Start_Legal(), + } +} + +func ResourceMappingUsbID_Legal() []string { + return []string{ + "aBc1234567890_-", + "Qwerty-1234_ABCDEFGHIJKLMNOPQRSTUVWXYZ", + "x1y2z3_4-5-6-7-8-9", + "HelloWorld_2023", + "Ab1_cd2_ef3-gh4-ij5", + "a-_-_-_-_-_-_-_-_-_-_-", + "snapshotName_2433242", + "A1_B2-C3_D4-E5_F6", + "Xyz-123_456_789-0", + "Test_Case-123_456_789_0", + "a_1", + "B-c_2-D", + "E3_f4-G5_H6-I7", + "JKL_MNO_PQR-STU_VWX_YZ0", + "aBgnhfjkfgdihfghudsfgio", + "Cdsdjfidshfuisdghfsgffghdsufsdhfgdsfuahs", + "Ef-gh", + "Ij-kl-mn", + "Op-qr-st-u-vw-xy-z0-12-34-56-78-90", + "Abcd_1234-EFGH_5678-IJKL_9012", + "M-n-Op-qR-sT-uV-wX-yZ", + "a_b-c_d_e-f_g_h_i_j_k_l_m_n-o-p-q-r-s-t-", + "Aa1_Bb2-Cc3_Dd4-Ee5_Ff6-Gg7_Hh8-Ii9", + "JjKkLl-MmNnOoPpQqRrSsTtUuVvWwXxYyZz01", + "A-1", + "B-2_C-3", + "D-4_E-5-F-6", + "G-7-H-8-I-9", + "J-0_K-1-L-2-M-3-N-4-O-5-P-6-Q-7-R-8-S-9", + "T-0_U-1-V-2-W-3-X-4-Y-5-Z-6-7-8-9-0", + "a2B", + "c4D", + "e6F-g8H-i0J", + "k2L-m4N-o6P-q8R-s0T", + "u2V-w4X-y6Z-01-23-45-67-89-0", + "Abc_1234-Def_5678-Ghi_9012-Jkl_3456-Mno_", + "Pqr_2345-Stu_6789-Vwx_0123-Yz0_4567", + "a-B", + "c-D_e-F", + "g-H_i-J-k-L", + "m-N-o-P_q-R-s-T-u-V-w-X-y-Z-0", + "A_1b2-C3d4_E5f6-G7h8_I9j0-K1l2-M3n4", + "O5p6-Q7r8-S9t0-U1v2-W3x4-Y5z6-01", + "A2b3-C4d5-E6f7-G8h9-I0j1-K2l3-M4n5-O6", + "P7q8-R9s0-T1u2-V3w4-X5y6-Z7-89-01-23-45-", + "Ab_12-cD_34-eF_56-gH_78-iJ_90-kL_12-mN_3", + "O5p6-Q7r8-S9t0-U1v2-W3x4-Y5z6-01-23-45", + "A7b8-C9d0-E1f2-G3h4-I5j6-K7l8-M9n0-O1p2-", + "S5t6-U7v8-W9x0-Y1z2-34-56-78-90-12-34-56", + "Ab1C_d2E-F3G_h4I-J5k6L-m7N-o8P-q9R-s0T-u", + ResourceMappingUsbID_Max_Legal(), + ResourceMappingUsbID_Min_Legal(), + ResourceMappingUsbID_Start_Legal(), + } +}