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:
parent
68132bb1fb
commit
28ae95bd09
@ -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
|
||||
|
110
proxmox/nodes/vms/custom_amdsev.go
Normal file
110
proxmox/nodes/vms/custom_amdsev.go
Normal 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
|
||||
}
|
@ -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"`
|
||||
|
@ -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{
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user