0
0
mirror of https://github.com/bpg/terraform-provider-proxmox.git synced 2025-06-30 02:31:10 +00:00

feat(vm): Add support for setting the VM TPM State device (#743)

* feat(vm): add support for setting the vm tpm state

Signed-off-by: Rui Lopes <rgl@ruilopes.com>

* feat(vm): add encoding / unmarshalling for CustomTPMState

Signed-off-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com>

* feat(vm): fix typos

Signed-off-by: Rui Lopes <rgl@ruilopes.com>

* feat(vm): fix vmGetTPMState

Signed-off-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com>

* feat(docs): add the vm tpm_state documentation

Signed-off-by: Rui Lopes <rgl@ruilopes.com>

* feat(docs): add the vm tpm_state example

Signed-off-by: Rui Lopes <rgl@ruilopes.com>

---------

Signed-off-by: Rui Lopes <rgl@ruilopes.com>
Signed-off-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com>
Co-authored-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com>
This commit is contained in:
Rui Lopes 2023-12-05 02:53:33 +00:00 committed by GitHub
parent d1f2093d39
commit 66bba2a027
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 285 additions and 1 deletions

View File

@ -65,6 +65,10 @@ resource "proxmox_virtual_environment_vm" "ubuntu_vm" {
type = "l26"
}
tpm_state {
version = "v2.0"
}
serial_device {}
}
@ -287,6 +291,11 @@ output "ubuntu_vm_public_key" {
distribution-specific and Microsoft Standard keys enrolled, if used with
EFI type=`4m`. Ignored for VMs with cpu.architecture=`aarch64` (defaults
to `false`).
- `tpm_state` - (Optional) The TPM state device.
- `datastore_id` (Optional) The identifier for the datastore to create
the disk in (defaults to `local-lvm`).
- `version` (Optional) TPM state device version. Can be `v1.2` or `v2.0`.
(defaults to `v2.0`).
- `hostpci` - (Optional) A host PCI device mapping (multiple blocks supported).
- `device` - (Required) The PCI device name for Proxmox, in form
of `hostpciX` where `X` is a sequential number from 0 to 3.

View File

@ -33,6 +33,11 @@ resource "proxmox_virtual_environment_vm" "example_template" {
type = "4m"
}
tpm_state {
datastore_id = local.datastore_id
version = "v2.0"
}
# disk {
# datastore_id = local.datastore_id
# file_id = proxmox_virtual_environment_file.ubuntu_cloud_image.id

View File

@ -239,6 +239,12 @@ func (r CustomStorageDevice) IsOwnedBy(vmID int) bool {
// CustomStorageDevices handles QEMU SATA device parameters.
type CustomStorageDevices map[string]CustomStorageDevice
// CustomTPMState handles QEMU TPM state parameters.
type CustomTPMState struct {
FileVolume string `json:"file" url:"file"`
Version *string `json:"version,omitempty" url:"version,omitempty"`
}
// CustomUSBDevice handles QEMU USB device parameters.
type CustomUSBDevice struct {
HostDevice *string `json:"host" url:"host"`
@ -349,6 +355,7 @@ type CreateRequestBody struct {
Tags *string `json:"tags,omitempty" url:"tags,omitempty"`
Template *types.CustomBool `json:"template,omitempty" url:"template,omitempty,int"`
TimeDriftFixEnabled *types.CustomBool `json:"tdf,omitempty" url:"tdf,omitempty,int"`
TPMState *CustomTPMState `json:"tpmstate0,omitempty" url:"tpmstate0,omitempty"`
USBDevices CustomUSBDevices `json:"usb,omitempty" url:"usb,omitempty"`
VGADevice *CustomVGADevice `json:"vga,omitempty" url:"vga,omitempty"`
VirtualCPUCount *int `json:"vcpus,omitempty" url:"vcpus,omitempty"`
@ -517,6 +524,7 @@ type GetResponseData struct {
Tags *string `json:"tags,omitempty"`
Template *types.CustomBool `json:"template,omitempty"`
TimeDriftFixEnabled *types.CustomBool `json:"tdf,omitempty"`
TPMState *CustomTPMState `json:"tpmstate0,omitempty"`
USBDevice0 *CustomUSBDevice `json:"usb0,omitempty"`
USBDevice1 *CustomUSBDevice `json:"usb1,omitempty"`
USBDevice2 *CustomUSBDevice `json:"usb2,omitempty"`
@ -1233,6 +1241,21 @@ func (r CustomStorageDevices) EncodeValues(_ string, v *url.Values) error {
return nil
}
// EncodeValues converts a CustomTPMState struct to a URL vlaue.
func (r CustomTPMState) EncodeValues(key string, v *url.Values) error {
values := []string{
fmt.Sprintf("file=%s", r.FileVolume),
}
if r.Version != nil {
values = append(values, fmt.Sprintf("version=%s", *r.Version))
}
v.Add(key, strings.Join(values, ","))
return nil
}
// EncodeValues converts a CustomUSBDevice struct to a URL vlaue.
func (r CustomUSBDevice) EncodeValues(key string, v *url.Values) error {
if r.HostDevice == nil && r.Mapping == nil {
@ -1707,6 +1730,33 @@ func (r *CustomPCIDevice) UnmarshalJSON(b []byte) error {
return nil
}
// UnmarshalJSON converts a CustomTPMState string to an object.
func (r *CustomTPMState) UnmarshalJSON(b []byte) error {
var s string
if err := json.Unmarshal(b, &s); err != nil {
return fmt.Errorf("failed to unmarshal CustomTPMState: %w", err)
}
pairs := strings.Split(s, ",")
for _, p := range pairs {
v := strings.Split(strings.TrimSpace(p), "=")
if len(v) == 1 {
r.FileVolume = v[0]
} else if len(v) == 2 {
switch v[0] {
case "file":
r.FileVolume = v[1]
case "version":
r.Version = &v[1]
}
}
}
return nil
}
// UnmarshalJSON converts a CustomUSBDevice string to an object.
func (r *CustomUSBDevice) UnmarshalJSON(b []byte) error {
var s string

View File

@ -78,6 +78,8 @@ const (
dvResourceVirtualEnvironmentVMEFIDiskFileFormat = "qcow2"
dvResourceVirtualEnvironmentVMEFIDiskType = "2m"
dvResourceVirtualEnvironmentVMEFIDiskPreEnrolledKeys = false
dvResourceVirtualEnvironmentVMTPMStateDatastoreID = "local-lvm"
dvResourceVirtualEnvironmentVMTPMStateVersion = "v2.0"
dvResourceVirtualEnvironmentVMInitializationDatastoreID = "local-lvm"
dvResourceVirtualEnvironmentVMInitializationInterface = ""
dvResourceVirtualEnvironmentVMInitializationDNSDomain = ""
@ -199,6 +201,9 @@ const (
mkResourceVirtualEnvironmentVMEFIDiskFileFormat = "file_format"
mkResourceVirtualEnvironmentVMEFIDiskType = "type"
mkResourceVirtualEnvironmentVMEFIDiskPreEnrolledKeys = "pre_enrolled_keys"
mkResourceVirtualEnvironmentVMTPMState = "tpm_state"
mkResourceVirtualEnvironmentVMTPMStateDatastoreID = "datastore_id"
mkResourceVirtualEnvironmentVMTPMStateVersion = "version"
mkResourceVirtualEnvironmentVMHostPCI = "hostpci"
mkResourceVirtualEnvironmentVMHostPCIDevice = "device"
mkResourceVirtualEnvironmentVMHostPCIDeviceID = "id"
@ -794,6 +799,38 @@ func VM() *schema.Resource {
MaxItems: 1,
MinItems: 0,
},
mkResourceVirtualEnvironmentVMTPMState: {
Type: schema.TypeList,
Description: "The tpmstate device",
Optional: true,
ForceNew: true,
DefaultFunc: func() (interface{}, error) {
return []interface{}{}, nil
},
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
mkResourceVirtualEnvironmentVMTPMStateDatastoreID: {
Type: schema.TypeString,
Description: "Datastore ID",
Optional: true,
Default: dvResourceVirtualEnvironmentVMTPMStateDatastoreID,
},
mkResourceVirtualEnvironmentVMTPMStateVersion: {
Type: schema.TypeString,
Description: "TPM version",
Optional: true,
ForceNew: true,
Default: dvResourceVirtualEnvironmentVMTPMStateVersion,
ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{
"v1.2",
"v2.0",
}, true)),
},
},
},
MaxItems: 1,
MinItems: 0,
},
mkResourceVirtualEnvironmentVMInitialization: {
Type: schema.TypeList,
Description: "The cloud-init configuration",
@ -2318,6 +2355,59 @@ func vmCreateClone(ctx context.Context, d *schema.ResourceData, m interface{}) d
}
}
tpmState := d.Get(mkResourceVirtualEnvironmentVMTPMState).([]interface{})
tpmStateInfo := vmGetTPMState(d, nil) // from the resource config
for i := range tpmState {
diskBlock := tpmState[i].(map[string]interface{})
diskInterface := "tpmstate0"
dataStoreID := diskBlock[mkResourceVirtualEnvironmentVMTPMStateDatastoreID].(string)
currentTPMState := vmConfig.TPMState
configuredTPMStateInfo := tpmStateInfo
if currentTPMState == nil {
diskUpdateBody := &vms.UpdateRequestBody{}
diskUpdateBody.TPMState = configuredTPMStateInfo
e = vmAPI.UpdateVM(ctx, diskUpdateBody)
if e != nil {
return diag.FromErr(e)
}
continue
}
deleteOriginalDisk := types.CustomBool(true)
diskMoveBody := &vms.MoveDiskRequestBody{
DeleteOriginalDisk: &deleteOriginalDisk,
Disk: diskInterface,
TargetStorage: dataStoreID,
}
moveDisk := false
if dataStoreID != "" {
moveDisk = true
if allDiskInfo[diskInterface] != nil {
fileIDParts := strings.Split(allDiskInfo[diskInterface].FileVolume, ":")
moveDisk = dataStoreID != fileIDParts[0]
}
}
if moveDisk {
moveDiskTimeout := d.Get(mkResourceVirtualEnvironmentVMTimeoutMoveDisk).(int)
e = vmAPI.MoveVMDisk(ctx, diskMoveBody, moveDiskTimeout)
if e != nil {
return diag.FromErr(e)
}
}
}
return vmCreateStart(ctx, d, m)
}
@ -2427,6 +2517,25 @@ func vmCreateCustom(ctx context.Context, d *schema.ResourceData, m interface{})
}
}
var tpmState *vms.CustomTPMState
tpmStateBlock := d.Get(mkResourceVirtualEnvironmentVMTPMState).([]interface{})
if len(tpmStateBlock) > 0 {
block := tpmStateBlock[0].(map[string]interface{})
datastoreID, _ := block[mkResourceVirtualEnvironmentVMTPMStateDatastoreID].(string)
version, _ := block[mkResourceVirtualEnvironmentVMTPMStateVersion].(string)
if version == "" {
version = dvResourceVirtualEnvironmentVMTPMStateVersion
}
tpmState = &vms.CustomTPMState{
FileVolume: fmt.Sprintf("%s:1", datastoreID),
Version: &version,
}
}
virtioDeviceObjects := diskDeviceObjects["virtio"]
scsiDeviceObjects := diskDeviceObjects["scsi"]
// ideDeviceObjects := getOrderedDiskDeviceList(diskDeviceObjects, "ide")
@ -2606,6 +2715,7 @@ func vmCreateCustom(ctx context.Context, d *schema.ResourceData, m interface{})
CPUUnits: &cpuUnits,
DedicatedMemory: &memoryDedicated,
EFIDisk: efiDisk,
TPMState: tpmState,
FloatingMemory: &memoryFloating,
IDEDevices: ideDevices,
KeyboardLayout: &keyboardLayout,
@ -3215,7 +3325,8 @@ func vmGetEfiDisk(d *schema.ResourceData, disk []interface{}) *vms.CustomEFIDisk
efiType, _ := block[mkResourceVirtualEnvironmentVMEFIDiskType].(string)
preEnrolledKeys := types.CustomBool(block[mkResourceVirtualEnvironmentVMEFIDiskPreEnrolledKeys].(bool))
// special case for efi disk, the size is ignored, see docs for more info
// use the special syntax STORAGE_ID:SIZE_IN_GiB to allocate a new volume.
// NB SIZE_IN_GiB is ignored, see docs for more info.
efiDiskConfig.FileVolume = fmt.Sprintf("%s:1", datastoreID)
efiDiskConfig.Format = &fileFormat
efiDiskConfig.Type = &efiType
@ -3256,6 +3367,54 @@ func vmGetEfiDiskAsStorageDevice(d *schema.ResourceData, disk []interface{}) (*v
return storageDevice, nil
}
func vmGetTPMState(d *schema.ResourceData, disk []interface{}) *vms.CustomTPMState {
var tpmState []interface{}
if disk != nil {
tpmState = disk
} else {
tpmState = d.Get(mkResourceVirtualEnvironmentVMTPMState).([]interface{})
}
var tpmStateConfig *vms.CustomTPMState
if len(tpmState) > 0 {
tpmStateConfig = &vms.CustomTPMState{}
block := tpmState[0].(map[string]interface{})
datastoreID, _ := block[mkResourceVirtualEnvironmentVMTPMStateDatastoreID].(string)
version, _ := block[mkResourceVirtualEnvironmentVMTPMStateVersion].(string)
// use the special syntax STORAGE_ID:SIZE_IN_GiB to allocate a new volume.
// NB SIZE_IN_GiB is ignored, see docs for more info.
tpmStateConfig.FileVolume = fmt.Sprintf("%s:1", datastoreID)
tpmStateConfig.Version = &version
}
return tpmStateConfig
}
func vmGetTPMStateAsStorageDevice(d *schema.ResourceData, disk []interface{}) *vms.CustomStorageDevice {
tpmState := vmGetTPMState(d, disk)
var storageDevice *vms.CustomStorageDevice
if tpmState != nil {
id := "0"
baseDiskInterface := "tpmstate"
diskInterface := fmt.Sprint(baseDiskInterface, id)
storageDevice = &vms.CustomStorageDevice{
Enabled: true,
FileVolume: tpmState.FileVolume,
Interface: &diskInterface,
ID: &id,
}
}
return storageDevice
}
func vmGetHostPCIDeviceObjects(d *schema.ResourceData) vms.CustomPCIDevices {
pciDevice := d.Get(mkResourceVirtualEnvironmentVMHostPCI).([]interface{})
pciDeviceObjects := make(vms.CustomPCIDevices, len(pciDevice))
@ -4101,6 +4260,29 @@ func vmReadCustom(
}
}
if vmConfig.TPMState != nil {
tpmState := map[string]interface{}{}
fileIDParts := strings.Split(vmConfig.TPMState.FileVolume, ":")
tpmState[mkResourceVirtualEnvironmentVMTPMStateDatastoreID] = fileIDParts[0]
tpmState[mkResourceVirtualEnvironmentVMTPMStateVersion] = dvResourceVirtualEnvironmentVMTPMStateVersion
currentTPMState := d.Get(mkResourceVirtualEnvironmentVMTPMState).([]interface{})
if len(clone) > 0 {
if len(currentTPMState) > 0 {
err := d.Set(mkResourceVirtualEnvironmentVMTPMState, []interface{}{tpmState})
diags = append(diags, diag.FromErr(err)...)
}
} else if len(currentTPMState) > 0 ||
tpmState[mkResourceVirtualEnvironmentVMTPMStateDatastoreID] != dvResourceVirtualEnvironmentVMTPMStateDatastoreID ||
tpmState[mkResourceVirtualEnvironmentVMTPMStateVersion] != dvResourceVirtualEnvironmentVMTPMStateVersion {
err := d.Set(mkResourceVirtualEnvironmentVMTPMState, []interface{}{tpmState})
diags = append(diags, diag.FromErr(err)...)
}
}
currentPCIList := d.Get(mkResourceVirtualEnvironmentVMHostPCI).([]interface{})
pciMap := map[string]interface{}{}
@ -5463,6 +5645,15 @@ func vmUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.D
rebootRequired = true
}
// Prepare the new tpm state configuration.
if d.HasChange(mkResourceVirtualEnvironmentVMTPMState) {
tpmState := vmGetTPMState(d, nil)
updateBody.TPMState = tpmState
rebootRequired = true
}
// Prepare the new cloud-init configuration.
stoppedBeforeUpdate := false
if d.HasChange(mkResourceVirtualEnvironmentVMInitialization) {
@ -5777,6 +5968,30 @@ func vmUpdateDiskLocationAndSize(
}
}
// Add tpm state if it has changes
if d.HasChange(mkResourceVirtualEnvironmentVMTPMState) {
diskOld, diskNew := d.GetChange(mkResourceVirtualEnvironmentVMTPMState)
oldTPMState := vmGetTPMStateAsStorageDevice(d, diskOld.([]interface{}))
newTPMState := vmGetTPMStateAsStorageDevice(d, diskNew.([]interface{}))
if oldTPMState != nil {
baseDiskInterface := diskDigitPrefix(*oldTPMState.Interface)
diskOldEntries[baseDiskInterface][*oldTPMState.Interface] = *oldTPMState
}
if newTPMState != nil {
baseDiskInterface := diskDigitPrefix(*newTPMState.Interface)
diskNewEntries[baseDiskInterface][*newTPMState.Interface] = *newTPMState
}
if oldTPMState != nil && newTPMState != nil && oldTPMState.Size != newTPMState.Size {
return diag.Errorf(
"resizing of tpm state is not supported.",
)
}
}
var diskMoveBodies []*vms.MoveDiskRequestBody
var diskResizeBodies []*vms.ResizeDiskRequestBody
@ -6052,6 +6267,11 @@ func getDiskDatastores(vm *vms.GetResponseData, d *schema.ResourceData) []string
datastoresSet[fileIDParts[0]] = 1
}
if vm.TPMState != nil {
fileIDParts := strings.Split(vm.TPMState.FileVolume, ":")
datastoresSet[fileIDParts[0]] = 1
}
datastores := []string{}
for datastore := range datastoresSet {
datastores = append(datastores, datastore)