Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: TPM State #322

Merged
merged 2 commits into from
Apr 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 36 additions & 3 deletions proxmox/config_qemu.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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))
}
Expand Down Expand Up @@ -840,20 +849,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 @@ -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 {
Expand Down Expand Up @@ -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 {
Expand All @@ -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
}
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())
})
}
}
Loading