diff --git a/docs/resources/virtual_environment_vm.md b/docs/resources/virtual_environment_vm.md index e91567f7..e7235fc6 100644 --- a/docs/resources/virtual_environment_vm.md +++ b/docs/resources/virtual_environment_vm.md @@ -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. diff --git a/example/resource_virtual_environment_vm.tf b/example/resource_virtual_environment_vm.tf index b5357219..0e25ef3f 100644 --- a/example/resource_virtual_environment_vm.tf +++ b/example/resource_virtual_environment_vm.tf @@ -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 diff --git a/proxmox/nodes/vms/vms_types.go b/proxmox/nodes/vms/vms_types.go index 11c36d79..8ee173b2 100644 --- a/proxmox/nodes/vms/vms_types.go +++ b/proxmox/nodes/vms/vms_types.go @@ -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 diff --git a/proxmoxtf/resource/vm.go b/proxmoxtf/resource/vm.go index f05d7ad2..a1fff42d 100644 --- a/proxmoxtf/resource/vm.go +++ b/proxmoxtf/resource/vm.go @@ -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)