Skip to content

Commit

Permalink
Merge pull request #322 from Tinyblargon/Feature#241
Browse files Browse the repository at this point in the history
Feature: TPM State
  • Loading branch information
Tinyblargon authored Apr 9, 2024
2 parents a76eb23 + 075ecad commit 32c480f
Show file tree
Hide file tree
Showing 4 changed files with 257 additions and 13 deletions.
39 changes: 36 additions & 3 deletions proxmox/config_qemu.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,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
Expand Down Expand Up @@ -401,6 +402,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 {
Expand Down Expand Up @@ -555,6 +561,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))
}
Expand Down Expand Up @@ -853,20 +862,21 @@ 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
}

var params map[string]interface{}
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
Expand All @@ -886,6 +896,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 {
Expand Down Expand Up @@ -993,7 +1015,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 {
Expand All @@ -1002,6 +1024,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
}
Expand Down
72 changes: 62 additions & 10 deletions proxmox/config_qemu_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand All @@ -5811,6 +5836,7 @@ func Test_ConfigQemu_mapToStruct(t *testing.T) {
})
}
}

func Test_ConfigQemu_Validate(t *testing.T) {
BandwidthValid0 := QemuDiskBandwidth{
MBps: QemuDiskBandwidthMBps{
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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)
})
}
}
}

Expand Down
94 changes: 94 additions & 0 deletions proxmox/config_qemu_tpm.go
Original file line number Diff line number Diff line change
@@ -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
}
65 changes: 65 additions & 0 deletions proxmox/config_qemu_tpm_test.go
Original file line number Diff line number Diff line change
@@ -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())
})
}
}

0 comments on commit 32c480f

Please sign in to comment.