From 7dfe635b62bb76cec66270c9b057f5ca0e31d97a Mon Sep 17 00:00:00 2001 From: wanyaoqi <18528551+wanyaoqi@users.noreply.github.com> Date: Mon, 29 Dec 2025 11:18:56 +0800 Subject: [PATCH] fix(region,host,host-deployer): add win11 tpm support (#23952) --- cmd/climc/shell/compute/servers.go | 1 + pkg/apis/compute/api.go | 3 +- pkg/apis/compute/guest_const.go | 2 + pkg/compute/models/guest_actions.go | 9 ++++ pkg/compute/models/guests.go | 14 +++++ pkg/hostman/guestman/desc/desc.go | 9 ++++ pkg/hostman/guestman/pci.go | 1 + pkg/hostman/guestman/qemu-kvmhelper.go | 71 +++++++++++++++++++++++++ pkg/hostman/guestman/qemu/generate.go | 13 +++++ pkg/hostman/guestman/qemu/qemu.go | 19 ++++--- pkg/hostman/options/options.go | 9 ++-- pkg/mcclient/options/compute/servers.go | 11 ++++ pkg/util/winutils/winutils.go | 15 ++++++ 13 files changed, 166 insertions(+), 11 deletions(-) diff --git a/cmd/climc/shell/compute/servers.go b/cmd/climc/shell/compute/servers.go index e4a7670f01..dc4a63aa60 100644 --- a/cmd/climc/shell/compute/servers.go +++ b/cmd/climc/shell/compute/servers.go @@ -131,6 +131,7 @@ func init() { cmd.BatchPerform("sync-os-info", &options.ServerIdsOptions{}) cmd.BatchPerform("set-root-disk-matcher", &options.ServerSetRootDiskMatcher{}) cmd.Perform("disable-auto-merge-snapshot", &options.ServerDisableAutoMergeSnapshot{}) + cmd.BatchPerform("set-tpm", &options.ServerSetTpmOptions{}) cmd.Perform("set-kickstart", &options.ServerKickstartConfigOptions{}) cmd.Perform("delete-kickstart", &options.ServerIdOptions{}) cmd.Perform("kickstart-complete", &options.ServerKickstartCompleteOptions{}) diff --git a/pkg/apis/compute/api.go b/pkg/apis/compute/api.go index 82b0adbf5b..1f49960284 100644 --- a/pkg/apis/compute/api.go +++ b/pkg/apis/compute/api.go @@ -566,7 +566,8 @@ type ServerCreateInput struct { // BIOS类型, 若镜像是Windows,并且支持UEFI,则自动会设置为UEFI // emulate: BIOS, UEFI - Bios string `json:"bios"` + Bios string `json:"bios"` + EnableTpm bool `json:"enable_tpm"` // Machine类型 // emulate: pc, q35 diff --git a/pkg/apis/compute/guest_const.go b/pkg/apis/compute/guest_const.go index 49a263a365..7ef11d092c 100644 --- a/pkg/apis/compute/guest_const.go +++ b/pkg/apis/compute/guest_const.go @@ -348,6 +348,8 @@ const ( VM_METADATA_START_VCPU_COUNT = "start_vcpu_count" VM_METADATA_DISABLE_AUTO_MERGE_SNAPSHOT = "disable_auto_merge_snapshot" + VM_METADATA_ENABLE_TPM = "enable_tpm" + VM_METADATA_RELEASED_DEVICES = "released_devices" VM_METADATA_CPU_NUMA_PIN = "__cpu_numa_pin" diff --git a/pkg/compute/models/guest_actions.go b/pkg/compute/models/guest_actions.go index 84dbd3c3b4..6794078114 100644 --- a/pkg/compute/models/guest_actions.go +++ b/pkg/compute/models/guest_actions.go @@ -6912,6 +6912,15 @@ func (self *SGuest) PerformEnableMemclean(ctx context.Context, userCred mcclient return nil, self.SetMetadata(ctx, api.VM_METADATA_ENABLE_MEMCLEAN, "true", userCred) } +func (self *SGuest) PerformSetTpm(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) (jsonutils.JSONObject, error) { + enableTpm := jsonutils.QueryBoolean(data, api.VM_METADATA_ENABLE_TPM, false) + if enableTpm { + return nil, self.SetMetadata(ctx, api.VM_METADATA_ENABLE_TPM, enableTpm, userCred) + } else { + return nil, self.RemoveMetadata(ctx, api.VM_METADATA_ENABLE_TPM, userCred) + } +} + // 设置操作系统信息 func (self *SGuest) PerformSetOsInfo(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input api.ServerSetOSInfoInput) (jsonutils.JSONObject, error) { drv, err := self.GetDriver() diff --git a/pkg/compute/models/guests.go b/pkg/compute/models/guests.go index 22e4599043..dd53307f04 100644 --- a/pkg/compute/models/guests.go +++ b/pkg/compute/models/guests.go @@ -1812,6 +1812,17 @@ func (manager *SGuestManager) validateCreateData( input.OsArch = apis.OS_ARCH_AARCH64 } + // enable tpm on windows 11 image + if osDist := imgProperties["os_distribution"]; strings.Contains(osDist, "Windows 11") { + input.EnableTpm = true + } + + // use uefi boot and q35 machine type on enable tpm + if input.EnableTpm { + input.Bios = "UEFI" + input.Machine = api.VM_MACHINE_TYPE_Q35 + } + if imageDiskFormat != "iso" { var imgSupportUEFI *bool var imgSupportBIOS *bool @@ -2717,6 +2728,9 @@ func (guest *SGuest) PostCreate(ctx context.Context, userCred mcclient.TokenCred if jsonutils.QueryBoolean(data, api.VM_METADATA_ENABLE_MEMCLEAN, false) { guest.SetMetadata(ctx, api.VM_METADATA_ENABLE_MEMCLEAN, "true", userCred) } + if jsonutils.QueryBoolean(data, api.VM_METADATA_ENABLE_TPM, false) { + guest.SetMetadata(ctx, api.VM_METADATA_ENABLE_TPM, "true", userCred) + } if jsonutils.QueryBoolean(data, imageapi.IMAGE_DISABLE_USB_KBD, false) { guest.SetMetadata(ctx, imageapi.IMAGE_DISABLE_USB_KBD, "true", userCred) } diff --git a/pkg/hostman/guestman/desc/desc.go b/pkg/hostman/guestman/desc/desc.go index 4e6da771b2..a10d28b99f 100644 --- a/pkg/hostman/guestman/desc/desc.go +++ b/pkg/hostman/guestman/desc/desc.go @@ -116,6 +116,8 @@ type SGuestHardwareDesc struct { VirtioSerial *SGuestVirtioSerial + Tpm *SGuestTpm + // std virtio cirrus vmware qlx none Vga string VgaDevice *SGuestVga `json:",omitempty"` @@ -173,6 +175,13 @@ type SGuestPvpanic struct { Id string } +type SGuestTpm struct { + TpmSock *CharDev + + // default emulator tpm + Id string +} + // -device pcie-pci-bridge,id=pci.1,bus=pcie.0 \ // -device pci-bridge,id=pci.2,bus=pci.1,chassis_nr=1,addr=0x01 \ diff --git a/pkg/hostman/guestman/pci.go b/pkg/hostman/guestman/pci.go index 57a47bda9f..66ac7e0ea0 100644 --- a/pkg/hostman/guestman/pci.go +++ b/pkg/hostman/guestman/pci.go @@ -99,6 +99,7 @@ func (s *SKVMGuestInstance) initGuestDevicesDesc(pciRoot, pciBridge *desc.PCICon s.initQgaDesc() s.initPvpanicDesc() s.initIsaSerialDesc() + s.initTpmDesc() return nil } diff --git a/pkg/hostman/guestman/qemu-kvmhelper.go b/pkg/hostman/guestman/qemu-kvmhelper.go index be7579e89d..f1dca5f84d 100644 --- a/pkg/hostman/guestman/qemu-kvmhelper.go +++ b/pkg/hostman/guestman/qemu-kvmhelper.go @@ -299,6 +299,10 @@ func (s *SKVMGuestInstance) disablePvpanicDev() bool { return s.Desc.Metadata["disable_pvpanic"] == "true" } +func (s *SKVMGuestInstance) enableTpmDev() bool { + return s.Desc.Metadata[api.VM_METADATA_ENABLE_TPM] == "true" +} + func (s *SKVMGuestInstance) getQuorumChildIndex() int64 { if sidx, ok := s.Desc.Metadata[api.QUORUM_CHILD_INDEX]; ok { idx, _ := strconv.ParseInt(sidx, 10, 0) @@ -364,6 +368,7 @@ func (s *SKVMGuestInstance) generateStartScript(data *jsonutils.JSONDict) (strin HomeDir: s.HomeDir(), HugepagesEnabled: s.manager.host.IsHugepagesEnabled(), EnableMemfd: s.isMemcleanEnabled(), + EnableTpm: s.enableTpmDev(), PidFilePath: s.GetPidFilePath(), } @@ -491,6 +496,28 @@ function nic_mtu() { } ` + if input.EnableTpm { + input.OVMFPath = options.HostOptions.SecbootOvmfPath + input.OVMFVarsPath = options.HostOptions.SecbootOvmfVarsPath + cmd += ` +function start_swtpm() { + local swtpm_binary=$1 + local swtpm_dir=$2 + local swtpm_socket=$swtpm_dir/swtpm.sock + local swtpm_log=$swtpm_dir/swtpm.log + local swtpm_pid=$swtpm_dir/swtpm.pid + + if [ -f "$swtpm_pid" ] && ps -p $(cat "$swtpm_pid") >/dev/null 2>&1; then + return 0 + fi + + mkdir -p $swtpm_dir + $swtpm_binary socket --tpmstate dir=$swtpm_dir --ctrl type=unixio,path=$swtpm_socket --log file=$swtpm_log,level=20 --pid file=$swtpm_pid --tpm2 -d +} +` + cmd += fmt.Sprintf("start_swtpm %s %s\n", options.HostOptions.BinarySwtpmPath, s.getSwtpmDirPath()) + } + // Generate Start VM script cmd += `CMD="$QEMU_CMD $QEMU_CMD_KVM_ARG` @@ -935,6 +962,21 @@ func (s *SKVMGuestInstance) generateStopScript(data *jsonutils.JSONDict) string cmd += " fi\n" cmd += "done\n" + if s.enableTpmDev() { + // stop swtpm + cmd += fmt.Sprintf("SWTPM_PID_FILE=%s\n", s.getSwtpmPidPath()) + cmd += "if [ -f $SWTPM_PID_FILE ]; then\n" + cmd += " SWTPM_PID=`cat $SWTPM_PID_FILE`\n" + cmd += " ps -p $SWTPM_PID > /dev/null\n" + cmd += " if [ $? -eq 0 ]; then\n" + cmd += " echo \"Kill swtpm process $SWTPM_PID\"\n" + cmd += " kill -9 $SWTPM_PID > /dev/null 2>&1\n" + cmd += " fi\n" + cmd += " echo \"Remove swtpm PID $SWTPM_PID_FILE\"\n" + cmd += " rm -f $SWTPM_PID_FILE\n" + cmd += "fi\n" + } + for _, nic := range nics { if nic.Driver == api.NETWORK_DRIVER_VFIO { dev, _ := s.GetSriovDeviceByNetworkIndex(nic.Index) @@ -1001,6 +1043,22 @@ func (s *SKVMGuestInstance) getPKIDirPath() string { return path.Join(s.HomeDir(), "pki") } +func (s *SKVMGuestInstance) getSwtpmDirPath() string { + return path.Join(s.HomeDir(), "swtpm") +} + +func (s *SKVMGuestInstance) getSwtpmSocketPath() string { + return path.Join(s.getSwtpmDirPath(), "swtpm.sock") +} + +func (s *SKVMGuestInstance) getSwtpmLogPath() string { + return path.Join(s.getSwtpmDirPath(), "swtpm.log") +} + +func (s *SKVMGuestInstance) getSwtpmPidPath() string { + return path.Join(s.getSwtpmDirPath(), "swtpm.pid") +} + func (s *SKVMGuestInstance) makePKIDir() error { output, err := procutils.NewCommand("mkdir", "-p", s.getPKIDirPath()).Output() if err != nil { @@ -1312,6 +1370,19 @@ func (s *SKVMGuestInstance) initIsaSerialDesc() { } } +func (s *SKVMGuestInstance) initTpmDesc() { + if s.enableTpmDev() { + charDevId := "chrtpm" + s.Desc.Tpm = &desc.SGuestTpm{ + TpmSock: desc.NewCharDev("socket", charDevId, ""), + Id: "tpm0", + } + s.Desc.Tpm.TpmSock.Options = map[string]string{ + "path": s.getSwtpmSocketPath(), + } + } +} + func (s *SKVMGuestInstance) getVfioDeviceHotPlugPciControllerType() *desc.PCI_CONTROLLER_TYPE { if s.Desc.Machine == api.VM_MACHINE_TYPE_Q35 || s.Desc.Machine == api.VM_MACHINE_TYPE_ARM_VIRT { _, _, found := s.findUnusedSlotForController(desc.CONTROLLER_TYPE_PCIE_ROOT_PORT, 0) diff --git a/pkg/hostman/guestman/qemu/generate.go b/pkg/hostman/guestman/qemu/generate.go index 525f545b07..f3c9e061ef 100644 --- a/pkg/hostman/guestman/qemu/generate.go +++ b/pkg/hostman/guestman/qemu/generate.go @@ -666,6 +666,14 @@ func generatePvpanicDeviceOption(pvpanic *desc.SGuestPvpanic) string { return fmt.Sprintf("-device pvpanic,id=%s,ioport=0x%x", pvpanic.Id, pvpanic.Ioport) } +func generateTpmDevOptions(tpm *desc.SGuestTpm) []string { + opts := make([]string, 0) + opts = append(opts, chardevOption(tpm.TpmSock)) + opts = append(opts, fmt.Sprintf("-tpmdev emulator,id=%s,chardev=%s", tpm.Id, tpm.TpmSock.Id)) + opts = append(opts, fmt.Sprintf("-device tpm-tis,tpmdev=%s", tpm.Id)) + return opts +} + func getMigrateOptions(drvOpt QemuOptions, input *GenerateStartOptionsInput) []string { opts := make([]string, 0) if input.NeedMigrate { @@ -692,6 +700,7 @@ type GenerateStartOptionsInput struct { OsName string HugepagesEnabled bool EnableMemfd bool + EnableTpm bool OVNIntegrationBridge string Devices []string @@ -939,6 +948,10 @@ func GenerateStartOptions( opts = append(opts, generatePvpanicDeviceOption(input.GuestDesc.Pvpanic)) } + if input.GuestDesc.Tpm != nil { + opts = append(opts, generateTpmDevOptions(input.GuestDesc.Tpm)...) + } + // move extra options to end of cmdline if len(input.ExtraOptions) != 0 { opts = append(opts, input.ExtraOptions...) diff --git a/pkg/hostman/guestman/qemu/qemu.go b/pkg/hostman/guestman/qemu/qemu.go index 3d10ba8365..cd4db794d9 100644 --- a/pkg/hostman/guestman/qemu/qemu.go +++ b/pkg/hostman/guestman/qemu/qemu.go @@ -17,6 +17,7 @@ package qemu import ( "fmt" "path" + "path/filepath" "strings" "sync" @@ -273,20 +274,24 @@ func (o baseOptions) Boot(order *string, enableMenu bool) string { } func (o baseOptions) BIOS(ovmfPath, ovmfVarsPath, homedir string) (string, error) { - if ovmfVarsPath == "" || !fileutils2.Exists(ovmfVarsPath) { - ovmfVarsPath = ovmfPath + ovmfVarsName := "OVMF_VARS.fd" + if ovmfVarsPath != "" { + ovmfVarsName = filepath.Base(ovmfVarsPath) } - - destOvmfVarsPath := path.Join(homedir, "OVMF_VARS.fd") - if !fileutils2.Exists(destOvmfVarsPath) { - err := procutils.NewRemoteCommandAsFarAsPossible("cp", "-f", ovmfVarsPath, destOvmfVarsPath).Run() + guestOvmfVarsPath := path.Join(homedir, ovmfVarsName) + if !fileutils2.Exists(guestOvmfVarsPath) { + sourceOvmfVarsPath := ovmfPath + if ovmfVarsPath != "" { + sourceOvmfVarsPath = ovmfVarsPath + } + err := procutils.NewRemoteCommandAsFarAsPossible("cp", "-f", sourceOvmfVarsPath, guestOvmfVarsPath).Run() if err != nil { return "", errors.Wrap(err, "failed copy ovmf vars") } } return fmt.Sprintf( "-drive if=pflash,format=raw,unit=0,file=%s,readonly=on -drive if=pflash,format=raw,unit=1,file=%s", - ovmfPath, destOvmfVarsPath, + ovmfPath, guestOvmfVarsPath, ), nil } diff --git a/pkg/hostman/options/options.go b/pkg/hostman/options/options.go index 75cafad993..908fa9a74c 100644 --- a/pkg/hostman/options/options.go +++ b/pkg/hostman/options/options.go @@ -113,9 +113,11 @@ type SHostOptions struct { DnsServer string `help:"Address of host DNS server"` DnsServerLegacy string `help:"Deprecated Address of host DNS server"` - ChntpwPath string `help:"path to chntpw tool" default:"/usr/local/bin/chntpw.static"` - OvmfPath string `help:"Path to OVMF.fd" default:"/opt/cloud/contrib/OVMF.fd"` - OvmfVarsPath string `help:"Path to OVMF_VARS.fd" default:"/opt/cloud/contrib/OVMF_VARS.fd"` + ChntpwPath string `help:"path to chntpw tool" default:"/usr/local/bin/chntpw.static"` + OvmfPath string `help:"Path to OVMF.fd" default:"/opt/cloud/contrib/OVMF.fd"` + OvmfVarsPath string `help:"Path to OVMF_VARS.fd" default:"/opt/cloud/contrib/OVMF_VARS.fd"` + SecbootOvmfPath string `help:"Path to secboot ovmf fd" default:"/opt/cloud/contrib/OVMF_CODE_4M.secboot.fd"` + SecbootOvmfVarsPath string `help:"Path to secboot ovmf vars fd" default:"/opt/cloud/contrib/OVMF_VARS_4M.fd"` LinuxDefaultRootUser bool `help:"Default account for linux system is root"` WindowsDefaultAdminUser bool `default:"true" help:"Default account for Windows system is Administrator"` @@ -234,6 +236,7 @@ type SHostOptions struct { LocalBackupTempPath string `help:"the local temporary directory for backup" default:"/opt/cloud/workspace/run/backups"` BinaryMemcleanPath string `help:"execute binary memclean path" default:"/opt/yunion/bin/memclean"` + BinarySwtpmPath string `help:"swtpm binary path" default:"/usr/bin/swtpm"` MaxHotplugVCpuCount int `help:"maximal possible vCPU count that the platform kvm supports"` PcieRootPortCount int `help:"pcie root port count" default:"2"` diff --git a/pkg/mcclient/options/compute/servers.go b/pkg/mcclient/options/compute/servers.go index 6da46d773a..885a67f1c4 100644 --- a/pkg/mcclient/options/compute/servers.go +++ b/pkg/mcclient/options/compute/servers.go @@ -451,6 +451,7 @@ type ServerCreateOptionalOptions struct { MemSpec string `help:"Memory size Or Instance Type" metavar:"MEMSPEC" json:"-"` CpuSockets int `help:"Cpu sockets"` EnableMemclean bool `help:"clean guest memory after guest exit" json:"enable_memclean"` + EnableTpm bool `help:"enable tpm device" json:"enable_tpm"` Keypair string `help:"SSH Keypair"` Password string `help:"Default user password"` @@ -598,6 +599,7 @@ func (opts *ServerCreateOptionalOptions) OptionalParams() (*computeapi.ServerCre GuestImageID: opts.GuestImageID, Secgroups: opts.Secgroups, EnableMemclean: opts.EnableMemclean, + EnableTpm: opts.EnableTpm, } params.ProjectId = opts.Project @@ -1668,6 +1670,15 @@ func (o *ServerSetRootDiskMatcher) Params() (jsonutils.JSONObject, error) { return jsonutils.Marshal(matcher), nil } +type ServerSetTpmOptions struct { + ServerIdsOptions + EnableTpm bool `help:"Enable tpm device"` +} + +func (o *ServerSetTpmOptions) Params() (jsonutils.JSONObject, error) { + return jsonutils.Marshal(o), nil +} + type ServerChangeBillingTypeOptions struct { ServerIdOptions BillingType string `choices:"prepaid|postpaid"` diff --git a/pkg/util/winutils/winutils.go b/pkg/util/winutils/winutils.go index 9d92a11324..55d5432edf 100644 --- a/pkg/util/winutils/winutils.go +++ b/pkg/util/winutils/winutils.go @@ -21,6 +21,7 @@ import ( "io/ioutil" "path" "regexp" + "strconv" "strings" "time" @@ -619,6 +620,20 @@ func (w *SWinRegTool) SetDnsServer(nameserver, searchlist string) { func (w *SWinRegTool) GetProductName() string { prodKey := `HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProductName` + prodName := w.GetRegistry(prodKey) + + if strings.Contains(prodName, "Windows 10") { + // check build number to determine whether it's actually Windows 11 + buildNumStr := w.GetCurrentBuildNumber() + if buildNum, err := strconv.Atoi(strings.TrimSpace(buildNumStr)); err == nil && buildNum >= 22000 { + prodName = strings.Replace(prodName, "Windows 10", "Windows 11", 1) + } + } + return prodName +} + +func (w *SWinRegTool) GetCurrentBuildNumber() string { + prodKey := `HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\CurrentBuildNumber` return w.GetRegistry(prodKey) }