0
0
mirror of https://github.com/bpg/terraform-provider-proxmox.git synced 2025-06-29 18:21:10 +00:00

feat(vm): add support for AMD SEV (#1952)

Signed-off-by: Anton Iacobaeus <anton.iacobaeus@canarybit.eu>
This commit is contained in:
Anton Iacobaeus 2025-05-13 03:43:15 +02:00 committed by GitHub
parent 68132bb1fb
commit 28ae95bd09
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 341 additions and 0 deletions

View File

@ -134,6 +134,20 @@ output "ubuntu_vm_public_key" {
- `type` - (Optional) The QEMU agent interface type (defaults to `virtio`).
- `isa` - ISA Serial Port.
- `virtio` - VirtIO (paravirtualized).
- `amd_sev` - (Optional) Secure Encrypted Virtualization (SEV) features by AMD CPUs.
- `type` - (Optional) Enable standard SEV with `std` or enable experimental
SEV-ES with the `es` option or enable experimental SEV-SNP with the `snp` option
(defaults to `std`).
- `allow_smt` - (Optional) Sets policy bit to allow Simultaneous Multi Threading (SMT)
(Ignored unless for SEV-SNP) (defaults to `true`).
- `kernel_hashes` - (Optional) Add kernel hashes to guest firmware for measured
linux kernel launch (defaults to `false`).
- `no_debug` - (Optional) Sets policy bit to disallow debugging of guest (defaults
to `false`).
- `no_key_sharing` - (Optional) Sets policy bit to disallow key sharing with
other guests (Ignored for SEV-SNP) (defaults to `false`).
The `amd_sev` setting is only allowed for a `root@pam` authenticated user.
- `audio_device` - (Optional) An audio device.
- `device` - (Optional) The device (defaults to `intel-hda`).
- `AC97` - Intel 82801AA AC97 Audio.
@ -642,6 +656,27 @@ and when refreshing resources. The provider has no way to distinguish between
trusts the user to set `agent.enabled` correctly and waits for
`qemu-guest-agent` to start.
## AMD SEV
AMD SEV (-ES, -SNP) are security features for AMD processors. SEV-SNP support
is included in Proxmox version **8.4**, see [Proxmox Wiki](
https://pve.proxmox.com/wiki/Qemu/KVM_Virtual_Machines#qm_virtual_machines_settings)
and [Proxmox Documentation](https://pve.proxmox.com/pve-docs/pve-admin-guide.html#qm_memory_encryption)
for more information.
`amd-sev` requires root and therefore `root@pam` auth.
SEV-SNP requires `bios = OVMF` and a supported AMD CPU (`EPYC-v4` for instance),
`machine = q35` is also advised. No EFI disk is required since SEV-SNP uses
consolidated read-only firmware. A configured EFI will be ignored.
All changes made to `amd_sev` will trigger reboots. Removing or adding the
`amd_sev` block will force a replacement of the resource. Modifying the `amd_sev`
block will not trigger replacements.
`allow_smt` is by default set to `true` even if `snp` is not the selected type.
Proxmox will ignore this value when `snp` is not in use. Likewise `no_key_sharing`
is `false` by default but ignored by Proxmox when `snp` is in use.
## Important Notes
### `local-lvm` Datastore

View File

@ -0,0 +1,110 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package vms
import (
"encoding/json"
"fmt"
"net/url"
"strings"
"github.com/bpg/terraform-provider-proxmox/proxmox/types"
)
// CustomAMDSEV handles AMDSEV parameters.
type CustomAMDSEV struct {
Type string `json:"type" url:"type"`
AllowSMT *types.CustomBool `json:"allow-smt" url:"allow-smt,int"`
KernelHashes *types.CustomBool `json:"kernel-hashes" url:"kernel-hashes,int"`
NoDebug *types.CustomBool `json:"no-debug" url:"no-debug,int"`
NoKeySharing *types.CustomBool `json:"no-key-sharing" url:"no-key-sharing,int"`
}
// EncodeValues converts a CustomAMDSEV struct to a URL value.
func (r *CustomAMDSEV) EncodeValues(key string, v *url.Values) error {
values := []string{
fmt.Sprintf("type=%s", r.Type),
}
if r.AllowSMT != nil {
if *r.AllowSMT {
values = append(values, "allow-smt=1")
} else {
values = append(values, "allow-smt=0")
}
}
if r.KernelHashes != nil {
if *r.KernelHashes {
values = append(values, "kernel-hashes=1")
} else {
values = append(values, "kernel-hashes=0")
}
}
if r.NoDebug != nil {
if *r.NoDebug {
values = append(values, "no-debug=1")
} else {
values = append(values, "no-debug=0")
}
}
if r.NoKeySharing != nil {
if *r.NoKeySharing {
values = append(values, "no-key-sharing=1")
} else {
values = append(values, "no-key-sharing=0")
}
}
if len(values) > 0 {
v.Add(key, strings.Join(values, ","))
}
return nil
}
// UnmarshalJSON converts a CustomAMDSEV string to an object.
func (r *CustomAMDSEV) UnmarshalJSON(b []byte) error {
var s string
if err := json.Unmarshal(b, &s); err != nil {
return fmt.Errorf("error unmarshalling CustomAMDSEV: %w", err)
}
pairs := strings.Split(s, ",")
for i, p := range pairs {
v := strings.Split(strings.TrimSpace(p), "=")
if len(v) == 1 && i == 0 {
r.Type = v[0]
}
if len(v) == 2 {
switch v[0] {
case "type":
r.Type = v[1]
case "allow-smt":
allow_smt := types.CustomBool(v[1] == "1")
r.AllowSMT = &allow_smt
case "kernel-hashes":
kernel_hashes := types.CustomBool(v[1] == "1")
r.KernelHashes = &kernel_hashes
case "no-debug":
no_debug := types.CustomBool(v[1] == "1")
r.NoDebug = &no_debug
case "no-key-sharing":
no_key_sharing := types.CustomBool(v[1] == "1")
r.NoKeySharing = &no_key_sharing
}
}
}
return nil
}

View File

@ -47,6 +47,7 @@ type CloneRequestBody struct {
type CreateRequestBody struct {
ACPI *types.CustomBool `json:"acpi,omitempty" url:"acpi,omitempty,int"`
Agent *CustomAgent `json:"agent,omitempty" url:"agent,omitempty"`
AMDSEV *CustomAMDSEV `json:"amd-sev,omitempty" url:"amd-sev,omitempty"`
AllowReboot *types.CustomBool `json:"reboot,omitempty" url:"reboot,omitempty,int"`
AudioDevices CustomAudioDevices `json:"audio,omitempty" url:"audio,omitempty"`
Autostart *types.CustomBool `json:"autostart,omitempty" url:"autostart,omitempty,int"`
@ -183,6 +184,7 @@ type GetResponseBody struct {
type GetResponseData struct {
ACPI *types.CustomBool `json:"acpi,omitempty"`
Agent *CustomAgent `json:"agent,omitempty"`
AMDSEV *CustomAMDSEV `json:"amd-sev,omitempty"`
AllowReboot *types.CustomBool `json:"reboot,omitempty"`
AudioDevice *CustomAudioDevice `json:"audio0,omitempty"`
Autostart *types.CustomBool `json:"autostart,omitempty"`

View File

@ -158,6 +158,11 @@ func QEMUAgentTypeValidator() schema.SchemaValidateDiagFunc {
return validation.ToDiagFunc(validation.StringInSlice([]string{"isa", "virtio"}, false))
}
// AMDSEVTypeValidator is a schema validation function for AMDSEV types.
func AMDSEVTypeValidator() schema.SchemaValidateDiagFunc {
return validation.ToDiagFunc(validation.StringInSlice([]string{"std", "es", "snp"}, false))
}
// KeyboardLayoutValidator is a schema validation function for keyboard layouts.
func KeyboardLayoutValidator() schema.SchemaValidateDiagFunc {
return validation.ToDiagFunc(validation.StringInSlice([]string{

View File

@ -49,6 +49,11 @@ const (
dvAgentTimeout = "15m"
dvAgentTrim = false
dvAgentType = "virtio"
dvAMDSEVType = "std"
dvAMDSEVAllowSMT = true
dvAMDSEVKernelHashes = false
dvAMDSEVNoDebug = false
dvAMDSEVNoKeySharing = false
dvAudioDeviceDevice = "intel-hda"
dvAudioDeviceDriver = "spice"
dvAudioDeviceEnabled = true
@ -153,6 +158,12 @@ const (
mkAgentTimeout = "timeout"
mkAgentTrim = "trim"
mkAgentType = "type"
mkAMDSEV = "amd_sev"
mkAMDSEVType = "type"
mkAMDSEVAllowSMT = "allow_smt"
mkAMDSEVKernelHashes = "kernel_hashes"
mkAMDSEVNoDebug = "no_debug"
mkAMDSEVNoKeySharing = "no_key_sharing"
mkAudioDevice = "audio_device"
mkAudioDeviceDevice = "device"
mkAudioDeviceDriver = "driver"
@ -389,6 +400,53 @@ func VM() *schema.Resource {
MaxItems: 1,
MinItems: 0,
},
mkAMDSEV: {
Type: schema.TypeList,
Description: "Secure Encrypted Virtualization (SEV) features by AMD CPUs",
Optional: true,
ForceNew: true,
DefaultFunc: func() (interface{}, error) {
return []interface{}{}, nil
},
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
mkAMDSEVType: {
Type: schema.TypeString,
Description: "Enable standard SEV with type=std or enable experimental SEV-ES with the es option" +
"or enable experimental SEV-SNP with the snp option.",
Optional: true,
Default: dvAMDSEVType,
ValidateDiagFunc: AMDSEVTypeValidator(),
},
mkAMDSEVAllowSMT: {
Type: schema.TypeBool,
Description: "Sets policy bit to allow Simultaneous Multi Threading (SMT) (Ignored unless for SEV-SNP)",
Optional: true,
Default: dvAMDSEVAllowSMT,
},
mkAMDSEVKernelHashes: {
Type: schema.TypeBool,
Description: "Add kernel hashes to guest firmware for measured linux kernel launch",
Optional: true,
Default: dvAMDSEVKernelHashes,
},
mkAMDSEVNoDebug: {
Type: schema.TypeBool,
Description: "Sets policy bit to disallow debugging of guest",
Optional: true,
Default: dvAMDSEVNoDebug,
},
mkAMDSEVNoKeySharing: {
Type: schema.TypeBool,
Description: "Sets policy bit to disallow key sharing with other guests (Ignored for SEV-SNP)",
Optional: true,
Default: dvAMDSEVNoKeySharing,
},
},
},
MaxItems: 1,
MinItems: 0,
},
mkKVMArguments: {
Type: schema.TypeString,
Description: "The args implementation",
@ -1932,6 +1990,7 @@ func vmCreateClone(ctx context.Context, d *schema.ResourceData, m interface{}) d
acpi := types.CustomBool(d.Get(mkACPI).(bool))
agent := d.Get(mkAgent).([]interface{})
amdsev := d.Get(mkAMDSEV).([]interface{})
bios := d.Get(mkBIOS).(string)
cdrom := d.Get(mkCDROM).([]interface{})
cpu := d.Get(mkCPU).([]interface{})
@ -1982,6 +2041,32 @@ func vmCreateClone(ctx context.Context, d *schema.ResourceData, m interface{}) d
}
}
if len(amdsev) > 0 && amdsev[0] != nil {
amdsevBlock := amdsev[0].(map[string]interface{})
amdsevType := amdsevBlock[mkAMDSEVType].(string)
amdsevAllowSMT := types.CustomBool(
amdsevBlock[mkAMDSEVAllowSMT].(bool),
)
amdsevKernelHashes := types.CustomBool(
amdsevBlock[mkAMDSEVKernelHashes].(bool),
)
amdsevNoDebug := types.CustomBool(
amdsevBlock[mkAMDSEVNoDebug].(bool),
)
amdsevNoKeySharing := types.CustomBool(
amdsevBlock[mkAMDSEVNoKeySharing].(bool),
)
updateBody.AMDSEV = &vms.CustomAMDSEV{
Type: amdsevType,
AllowSMT: &amdsevAllowSMT,
KernelHashes: &amdsevKernelHashes,
NoDebug: &amdsevNoDebug,
NoKeySharing: &amdsevNoKeySharing,
}
}
if kvmArguments != dvKVMArguments {
updateBody.KVMArguments = &kvmArguments
}
@ -2437,6 +2522,8 @@ func vmCreateCustom(ctx context.Context, d *schema.ResourceData, m interface{})
agentTrim := types.CustomBool(agentBlock[mkAgentTrim].(bool))
agentType := agentBlock[mkAgentType].(string)
amdsev := vmGetAMDSEVObject(d)
kvmArguments := d.Get(mkKVMArguments).(string)
audioDevices := vmGetAudioDeviceList(d)
@ -2718,6 +2805,7 @@ func vmCreateCustom(ctx context.Context, d *schema.ResourceData, m interface{})
TrimClonedDisks: &agentTrim,
Type: &agentType,
},
AMDSEV: amdsev,
AudioDevices: audioDevices,
BIOS: &bios,
Boot: &vms.CustomBoot{
@ -2869,6 +2957,39 @@ func vmCreateStart(ctx context.Context, d *schema.ResourceData, m interface{}) d
return vmRead(ctx, d, m)
}
func vmGetAMDSEVObject(d *schema.ResourceData) *vms.CustomAMDSEV {
var amdsev *vms.CustomAMDSEV
amdsevBlock := d.Get(mkAMDSEV).([]interface{})
if len(amdsevBlock) > 0 && amdsevBlock[0] != nil {
block := amdsevBlock[0].(map[string]interface{})
amdsevType := block[mkAMDSEVType].(string)
amdsevAllowSMT := types.CustomBool(
block[mkAMDSEVAllowSMT].(bool),
)
amdsevKernelHashes := types.CustomBool(
block[mkAMDSEVKernelHashes].(bool),
)
amdsevNoDebug := types.CustomBool(
block[mkAMDSEVNoDebug].(bool),
)
amdsevNoKeySharing := types.CustomBool(
block[mkAMDSEVNoKeySharing].(bool),
)
amdsev = &vms.CustomAMDSEV{
Type: amdsevType,
AllowSMT: &amdsevAllowSMT,
KernelHashes: &amdsevKernelHashes,
NoDebug: &amdsevNoDebug,
NoKeySharing: &amdsevNoKeySharing,
}
}
return amdsev
}
func vmGetAudioDeviceList(d *schema.ResourceData) vms.CustomAudioDevices {
devices := d.Get(mkAudioDevice).([]interface{})
list := make(vms.CustomAudioDevices, len(devices))
@ -3628,6 +3749,65 @@ func vmReadCustom(
}
}
// Compare the amdsev configuration to the one stored in the state.
currentAMDSEV := d.Get(mkAMDSEV).([]interface{})
//nolint:gocritic
if len(clone) == 0 || len(currentAMDSEV) > 0 {
if vmConfig.AMDSEV != nil {
amdsev := map[string]interface{}{}
amdsev[mkAMDSEVType] = vmConfig.AMDSEV.Type
if vmConfig.AMDSEV.AllowSMT != nil {
amdsev[mkAMDSEVAllowSMT] = bool(*vmConfig.AMDSEV.AllowSMT)
} else {
amdsev[mkAMDSEVAllowSMT] = false
}
if vmConfig.AMDSEV.KernelHashes != nil {
amdsev[mkAMDSEVKernelHashes] = bool(*vmConfig.AMDSEV.KernelHashes)
} else {
amdsev[mkAMDSEVKernelHashes] = false
}
if vmConfig.AMDSEV.NoDebug != nil {
amdsev[mkAMDSEVNoDebug] = bool(*vmConfig.AMDSEV.NoDebug)
} else {
amdsev[mkAMDSEVNoDebug] = false
}
if vmConfig.AMDSEV.NoKeySharing != nil {
amdsev[mkAMDSEVNoKeySharing] = bool(*vmConfig.AMDSEV.NoKeySharing)
} else {
amdsev[mkAMDSEVNoKeySharing] = false
}
if len(clone) > 0 {
if len(currentAMDSEV) > 0 {
err := d.Set(mkAMDSEV, []interface{}{amdsev})
diags = append(diags, diag.FromErr(err)...)
}
} else if len(currentAMDSEV) > 0 ||
amdsev[mkAMDSEVType] != dvAMDSEVType ||
amdsev[mkAMDSEVAllowSMT] != dvAMDSEVAllowSMT ||
amdsev[mkAMDSEVKernelHashes] != dvAMDSEVKernelHashes ||
amdsev[mkAMDSEVNoDebug] != dvAMDSEVNoDebug ||
amdsev[mkAMDSEVNoKeySharing] != dvAMDSEVNoKeySharing {
err := d.Set(mkAMDSEV, []interface{}{amdsev})
diags = append(diags, diag.FromErr(err)...)
}
} else if len(clone) > 0 {
if len(currentAMDSEV) > 0 {
err := d.Set(mkAMDSEV, []interface{}{})
diags = append(diags, diag.FromErr(err)...)
}
} else {
err := d.Set(mkAMDSEV, []interface{}{})
diags = append(diags, diag.FromErr(err)...)
}
}
// Compare the audio devices to those stored in the state.
currentAudioDevice := d.Get(mkAudioDevice).([]interface{})
@ -5061,6 +5241,15 @@ func vmUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.D
rebootRequired = true
}
// Prepare the new amdsev configuration.
if d.HasChange(mkAMDSEV) {
amdsev := vmGetAMDSEVObject(d)
updateBody.AMDSEV = amdsev
rebootRequired = true
}
// Prepare the new audio devices.
if d.HasChange(mkAudioDevice) {
updateBody.AudioDevices = vmGetAudioDeviceList(d)