diff --git a/proxmox/config_qemu.go b/proxmox/config_qemu.go index 48956a8b..c8dfafc2 100644 --- a/proxmox/config_qemu.go +++ b/proxmox/config_qemu.go @@ -87,6 +87,7 @@ type ConfigQemu struct { Smbios1 string `json:"smbios1,omitempty"` // TODO should be custom type with enum? Sshkeys string `json:"sshkeys,omitempty"` // TODO should be an array of strings Startup string `json:"startup,omitempty"` // TODO should be a struct? + TPM *TpmState `json:"tpm,omitempty"` Tablet *bool `json:"tablet,omitempty"` Tags string `json:"tags,omitempty"` // TODO should be an array of a custom type as there are character and length limitations VmID int `json:"vmid,omitempty"` // TODO should be a custom type as there are limitations @@ -391,6 +392,11 @@ func (config ConfigQemu) mapToApiValues(currentConfig ConfigQemu) (rebootRequire if config.Smbios1 != "" { params["smbios1"] = config.Smbios1 } + if config.TPM != nil { + if delete := config.TPM.mapToApi(params, currentConfig.TPM); delete != "" { + itemsToDelete = AddToList(itemsToDelete, delete) + } + } if config.Iso != nil { if config.Disks == nil { @@ -545,6 +551,9 @@ func (ConfigQemu) mapToStruct(vmr *VmRef, params map[string]interface{}) (*Confi if _, isSet := params["onboot"]; isSet { config.Onboot = util.Pointer(Itob(int(params["onboot"].(float64)))) } + if itemValue, isSet := params["tpmstate0"]; isSet { + config.TPM = TpmState{}.mapToSDK(itemValue.(string)) + } if _, isSet := params["cores"]; isSet { config.QemuCores = int(params["cores"].(float64)) } @@ -840,13 +849,13 @@ func (config *ConfigQemu) setVmr(vmr *VmRef) (err error) { return } +// currentConfig will be mutated func (newConfig ConfigQemu) setAdvanced(currentConfig *ConfigQemu, rebootIfNeeded bool, vmr *VmRef, client *Client) (rebootRequired bool, err error) { err = newConfig.setVmr(vmr) if err != nil { return } - err = newConfig.Validate() - if err != nil { + if err = newConfig.Validate(currentConfig); err != nil { return } @@ -854,6 +863,7 @@ func (newConfig ConfigQemu) setAdvanced(currentConfig *ConfigQemu, rebootIfNeede var exitStatus string if currentConfig != nil { // Update + // TODO implement tmp move and version change url := "/nodes/" + vmr.node + "/" + vmr.vmType + "/" + strconv.Itoa(vmr.vmId) + "/config" var itemsToDeleteBeforeUpdate string // this is for items that should be removed before they can be created again e.g. cloud-init disks. (convert to array when needed) stopped := false @@ -873,6 +883,18 @@ func (newConfig ConfigQemu) setAdvanced(currentConfig *ConfigQemu, rebootIfNeede itemsToDeleteBeforeUpdate = newConfig.Disks.cloudInitRemove(*currentConfig.Disks) } + if newConfig.TPM != nil && currentConfig.TPM != nil { // delete or move TPM + delete, disk := newConfig.TPM.markChanges(*currentConfig.TPM) + if delete != "" { // delete + itemsToDeleteBeforeUpdate = AddToList(itemsToDeleteBeforeUpdate, delete) + currentConfig.TPM = nil + } else if disk != nil { // move + if _, err := disk.move(true, vmr, client); err != nil { + return false, err + } + } + } + if itemsToDeleteBeforeUpdate != "" { err = client.Put(map[string]interface{}{"delete": itemsToDeleteBeforeUpdate}, url) if err != nil { @@ -980,7 +1002,7 @@ func (newConfig ConfigQemu) setAdvanced(currentConfig *ConfigQemu, rebootIfNeede return } -func (config ConfigQemu) Validate() (err error) { +func (config ConfigQemu) Validate(current *ConfigQemu) (err error) { // TODO test all other use cases // TODO has no context about changes caused by updating the vm if config.Disks != nil { @@ -989,6 +1011,17 @@ func (config ConfigQemu) Validate() (err error) { return } } + if config.TPM != nil { + if current == nil { + if err = config.TPM.Validate(nil); err != nil { + return + } + } else { + if err = config.TPM.Validate(current.TPM); err != nil { + return + } + } + } return } diff --git a/proxmox/config_qemu_test.go b/proxmox/config_qemu_test.go index 886643fe..8686a5ce 100644 --- a/proxmox/config_qemu_test.go +++ b/proxmox/config_qemu_test.go @@ -5,6 +5,7 @@ import ( "strings" "testing" + "github.com/Telmate/proxmox-api-go/internal/util" "github.com/Telmate/proxmox-api-go/test/data/test_data_qemu" "github.com/stretchr/testify/require" ) @@ -1274,6 +1275,20 @@ func Test_ConfigQemu_mapToApiValues(t *testing.T) { config: &ConfigQemu{Iso: &IsoFile{Storage: "test", File: "file.iso"}}, output: map[string]interface{}{"ide2": "test:iso/file.iso,media=cdrom"}, }, + // Create TPM + {name: "Create TPM", + config: &ConfigQemu{TPM: &TpmState{Storage: "test", Version: util.Pointer(TpmVersion_2_0)}}, + output: map[string]interface{}{"tpmstate0": "test:1,version=v2.0"}, + }, + // Delete + + // Delete TPM + {name: "Delete TPM", + config: &ConfigQemu{TPM: &TpmState{Delete: true}}, + output: map[string]interface{}{"delete": "tpmstate0"}}, + {name: "Delete TPM Full", + config: &ConfigQemu{TPM: &TpmState{Storage: "test", Version: util.Pointer(TpmVersion_2_0), Delete: true}}, + output: map[string]interface{}{"delete": "tpmstate0"}}, // Update // Update Disk.Ide @@ -3253,6 +3268,11 @@ func Test_ConfigQemu_mapToApiValues(t *testing.T) { config: &ConfigQemu{Iso: &IsoFile{Storage: "NewStorage", File: "file.iso"}}, output: map[string]interface{}{"ide2": "NewStorage:iso/file.iso,media=cdrom"}, }, + // Update TPM + {name: "Update TPM", + config: &ConfigQemu{TPM: &TpmState{Storage: "aaaa", Version: util.Pointer(TpmVersion_1_2)}}, + currentConfig: ConfigQemu{TPM: &TpmState{Storage: "test", Version: util.Pointer(TpmVersion_2_0)}}, + output: map[string]interface{}{}}, } for _, test := range tests { t.Run(test.name, func(*testing.T) { @@ -5799,6 +5819,11 @@ func Test_ConfigQemu_mapToStruct(t *testing.T) { vmr: &VmRef{vmId: 100}, output: &ConfigQemu{VmID: 100}, }, + // TPM + {name: "TPM", + input: map[string]interface{}{"tpmstate0": string("local-lvm:vm-101-disk-0,size=4M,version=v2.0")}, + output: &ConfigQemu{TPM: &TpmState{Storage: "local-lvm", Version: util.Pointer(TpmVersion("v2.0"))}}, + }, } for _, test := range tests { t.Run(test.name, func(*testing.T) { @@ -5811,6 +5836,7 @@ func Test_ConfigQemu_mapToStruct(t *testing.T) { }) } } + func Test_ConfigQemu_Validate(t *testing.T) { BandwidthValid0 := QemuDiskBandwidth{ MBps: QemuDiskBandwidthMBps{ @@ -5902,9 +5928,10 @@ func Test_ConfigQemu_Validate(t *testing.T) { } validCloudInit := QemuCloudInitDisk{Format: QemuDiskFormat_Raw, Storage: "Test"} testData := []struct { - name string - input ConfigQemu - err error + name string + input ConfigQemu + current *ConfigQemu + err error }{ // Valid // Valid Disks @@ -6015,6 +6042,15 @@ func Test_ConfigQemu_Validate(t *testing.T) { }}}, }}, }, + // Valid Tpm + {name: "Valid TPM Create", + input: ConfigQemu{TPM: &TpmState{Storage: "test", Version: util.Pointer(TpmVersion("v2.0"))}}}, + {name: "Valid TPM Update", + input: ConfigQemu{TPM: &TpmState{Storage: "test", Version: util.Pointer(TpmVersion("v2.0"))}}, + current: &ConfigQemu{TPM: &TpmState{Storage: "test", Version: util.Pointer(TpmVersion("v1.2"))}}}, + {name: "Valid TPM Update Version=nil", + input: ConfigQemu{TPM: &TpmState{Storage: "test"}}, + current: &ConfigQemu{TPM: &TpmState{Storage: "test", Version: util.Pointer(TpmVersion("v1.2"))}}}, // Invalid // Invalid Disks Mutually exclusive Ide {name: "Invalid Disks MutuallyExclusive Ide 0", @@ -7094,15 +7130,31 @@ func Test_ConfigQemu_Validate(t *testing.T) { input: ConfigQemu{Disks: &QemuStorages{VirtIO: &QemuVirtIODisks{Disk_13: &QemuVirtIOStorage{Passthrough: &QemuVirtIOPassthrough{File: "/dev/disk/by-id/scsi1", WorldWideName: "0x5004A3B2C1D0E0F1#"}}}}}, err: errors.New(Error_QemuWorldWideName_Invalid), }, + // invalid TMP + {name: "Invalid TPM errors.New(storage is required) Create", + input: ConfigQemu{TPM: &TpmState{Storage: ""}}, + err: errors.New("storage is required")}, + {name: "Invalid TPM errors.New(storage is required) Update", + input: ConfigQemu{TPM: &TpmState{Storage: ""}}, + current: &ConfigQemu{TPM: &TpmState{}}, + err: errors.New("storage is required")}, + {name: "Invalid TPM errors.New(TmpState_Error_VersionRequired) Create", + input: ConfigQemu{TPM: &TpmState{Storage: "test", Version: nil}}, + err: errors.New(TmpState_Error_VersionRequired)}, + {name: "Invalid TPM errors.New(TmpVersion_Error_Invalid) Create", + input: ConfigQemu{TPM: &TpmState{Storage: "test", Version: util.Pointer(TpmVersion(""))}}, + err: errors.New(TpmVersion_Error_Invalid)}, + {name: "Invalid TPM errors.New(TmpVersion_Error_Invalid) Update", + input: ConfigQemu{TPM: &TpmState{Storage: "test", Version: util.Pointer(TpmVersion(""))}}, + current: &ConfigQemu{TPM: &TpmState{}}, + err: errors.New(TpmVersion_Error_Invalid)}, } for _, test := range testData { - t.Run(test.name, func(*testing.T) { - if test.err != nil { - require.Equal(t, test.input.Validate(), test.err, test.name) - } else { - require.NoError(t, test.input.Validate(), test.name) - } - }) + if test.current == nil { + t.Run(test.name, func(*testing.T) { + require.Equal(t, test.input.Validate(test.current), test.err, test.name) + }) + } } } diff --git a/proxmox/config_qemu_tpm.go b/proxmox/config_qemu_tpm.go new file mode 100644 index 00000000..04d1a7c7 --- /dev/null +++ b/proxmox/config_qemu_tpm.go @@ -0,0 +1,94 @@ +package proxmox + +import ( + "errors" + "strings" + + "github.com/Telmate/proxmox-api-go/internal/util" +) + +type TpmState struct { + Delete bool `json:"remove,omitempty"` // If true, the tpmstate will be deleted. + Storage string `json:"storage"` // TODO change to proper type once the type is added. + Version *TpmVersion `json:"version,omitempty"` // Changing version will delete the current tpmstate and create a new one. Optional during update, required during create. +} + +const TmpState_Error_VersionRequired string = "version is required" + +func (t TpmState) mapToApi(params map[string]interface{}, currentTpm *TpmState) string { + if t.Delete { + return "tpmstate0" + } + if currentTpm == nil { // create + params["tpmstate0"] = t.Storage + ":1,version=" + t.Version.mapToApi() + } + return "" +} + +func (TpmState) mapToSDK(param string) *TpmState { + setting := splitStringOfSettings(param) + splitString := strings.Split(param, ":") + tmp := TpmState{} + if len(splitString) > 1 { + tmp.Storage = splitString[0] + } + if itemValue, isSet := setting["version"]; isSet { + tmp.Version = util.Pointer(TpmVersion(itemValue.(string))) + } + return &tmp + +} + +func (t TpmState) markChanges(currentTpm TpmState) (delete string, disk *qemuDiskMove) { + if t.Delete { + return "", nil + } + if t.Version != nil && t.Version.mapToApi() != string(*currentTpm.Version) { + return "tpmstate0", nil + } + if t.Storage != currentTpm.Storage { + return "", &qemuDiskMove{Storage: t.Storage, Id: "tpmstate0"} + } + return "", nil +} + +func (t TpmState) Validate(current *TpmState) error { + if t.Storage == "" { + return errors.New("storage is required") + } + if t.Version == nil { + if current == nil { // create + return errors.New(TmpState_Error_VersionRequired) + } + } else { + if err := t.Version.Validate(); err != nil { + return err + } + } + return nil +} + +type TpmVersion string // enum + +const ( + TpmVersion_1_2 TpmVersion = "v1.2" + TpmVersion_2_0 TpmVersion = "v2.0" + TpmVersion_Error_Invalid string = "enum TmpVersion should be one of: " + string(TpmVersion_1_2) + ", " + string(TpmVersion_2_0) +) + +func (t TpmVersion) mapToApi() string { + switch t { + case TpmVersion_1_2, "1.2": + return string(t) + case TpmVersion_2_0, "v2", "2.0", "2": + return string(TpmVersion_2_0) + } + return "" +} + +func (t TpmVersion) Validate() error { + if t.mapToApi() == "" { + return errors.New(TpmVersion_Error_Invalid) + } + return nil +} diff --git a/proxmox/config_qemu_tpm_test.go b/proxmox/config_qemu_tpm_test.go new file mode 100644 index 00000000..1ff8666a --- /dev/null +++ b/proxmox/config_qemu_tpm_test.go @@ -0,0 +1,65 @@ +package proxmox + +import ( + "errors" + "testing" + + "github.com/Telmate/proxmox-api-go/internal/util" + "github.com/stretchr/testify/require" +) + +func Test_TpmState_Validate(t *testing.T) { + type testInput struct { + config TpmState + current *TpmState + } + tests := []struct { + name string + input testInput + output error + }{ + {name: `Invalid Storage Create`, input: testInput{ + config: TpmState{Storage: ""}}, + output: errors.New("storage is required")}, + {name: `Invalid Storage Update`, input: testInput{ + config: TpmState{Storage: ""}, + current: &TpmState{Storage: "local-lvm"}}, + output: errors.New("storage is required")}, + {name: `Invalid Version=nil Create`, input: testInput{ + config: TpmState{Storage: "local-lvm"}}, + output: errors.New(TmpState_Error_VersionRequired)}, + {name: `Invalid Version="" Create`, input: testInput{ + config: TpmState{Storage: "local-lvm", Version: util.Pointer(TpmVersion(""))}}, + output: errors.New(TpmVersion_Error_Invalid)}, + {name: `Invalid Version="" Update`, input: testInput{ + config: TpmState{Storage: "local-lvm", Version: util.Pointer(TpmVersion(""))}, + current: &TpmState{Storage: "local-lvm", Version: util.Pointer(TpmVersion("v2.0"))}}, + output: errors.New(TpmVersion_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_TpmVersion_Validate(t *testing.T) { + tests := []struct { + name string + input TpmVersion + output error + }{ + {name: "Valid v1.2", input: TpmVersion_1_2}, + {name: "Valid v2.0", input: TpmVersion_2_0}, + {name: "Valid 1.2", input: "1.2"}, + {name: "Valid 2", input: "2"}, + {name: "Valid 2.0", input: "2.0"}, + {name: "Valid v2", input: "v2"}, + {name: `Invalid ""`, output: errors.New(TpmVersion_Error_Invalid)}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require.Equal(t, test.output, test.input.Validate()) + }) + } +}