diff --git a/.web-docs/components/builder/vsphere-clone/README.md b/.web-docs/components/builder/vsphere-clone/README.md index 7552a393..5cad5184 100644 --- a/.web-docs/components/builder/vsphere-clone/README.md +++ b/.web-docs/components/builder/vsphere-clone/README.md @@ -1497,6 +1497,13 @@ wget http://{{ .HTTPIP }}:{{ .HTTPPort }}/foo/bar/preseed.cfg - `vTPM` (bool) - Enable virtual trusted platform module (TPM) device for the virtual machine. Defaults to `false`. + + -> **Note:** A virtual machine with a vTPM cannot be exported as OVF/OVA + (`export`) or imported to a content library as an OVF template + (`content_library_destination` with `ovf` set to `true`). Set + [`remove_vtpm`](#remove_vtpm) to `true` to remove the device after + shutdown. A content library VM template (`ovf` unset or `false`) can keep + the vTPM. - `precision_clock` (string) - The virtual precision clock device for the virtual machine. Defaults to `none`. @@ -1506,6 +1513,20 @@ wget http://{{ .HTTPIP }}:{{ .HTTPPort }}/foo/bar/preseed.cfg + + +- `remove_vtpm` (bool) - Remove the virtual trusted platform module (vTPM) device from the virtual + machine after shutdown. Defaults to `false`. + + -> **Note:** A virtual machine with a vTPM cannot be exported as OVF/OVA + (`export`) or imported to a content library as an OVF template + (`content_library_destination` with `ovf` set to `true`). Set this option + to `true` to remove the device after shutdown. A content library VM + template (`ovf` unset or `false`) can keep the vTPM. + + + + ### Location Configuration **Optional:** @@ -2442,6 +2463,7 @@ Clone the default **Read-Only** vSphere role and add the following privileges: | ... | Download files | `ContentLibrary.DownloadSession` | | Cryptographic Operations | Direct access | `Cryptographer.Access` | | ... | Encrypt | `Cryptographer.Encrypt` | +| ... | Decrypt | `Cryptographer.Decrypt` | | Datastore | Allocate space | `Datastore.AllocateSpace` | | ... | Browse datastore | `Datastore.Browse` | | ... | Low level file operations | `Datastore.FileManagement` | diff --git a/.web-docs/components/builder/vsphere-iso/README.md b/.web-docs/components/builder/vsphere-iso/README.md index ee1282b6..96830f6d 100644 --- a/.web-docs/components/builder/vsphere-iso/README.md +++ b/.web-docs/components/builder/vsphere-iso/README.md @@ -277,6 +277,13 @@ wget http://{{ .HTTPIP }}:{{ .HTTPPort }}/foo/bar/preseed.cfg - `vTPM` (bool) - Enable virtual trusted platform module (TPM) device for the virtual machine. Defaults to `false`. + + -> **Note:** A virtual machine with a vTPM cannot be exported as OVF/OVA + (`export`) or imported to a content library as an OVF template + (`content_library_destination` with `ovf` set to `true`). Set + [`remove_vtpm`](#remove_vtpm) to `true` to remove the device after + shutdown. A content library VM template (`ovf` unset or `false`) can keep + the vTPM. - `precision_clock` (string) - The virtual precision clock device for the virtual machine. Defaults to `none`. @@ -286,6 +293,20 @@ wget http://{{ .HTTPIP }}:{{ .HTTPPort }}/foo/bar/preseed.cfg + + +- `remove_vtpm` (bool) - Remove the virtual trusted platform module (vTPM) device from the virtual + machine after shutdown. Defaults to `false`. + + -> **Note:** A virtual machine with a vTPM cannot be exported as OVF/OVA + (`export`) or imported to a content library as an OVF template + (`content_library_destination` with `ovf` set to `true`). Set this option + to `true` to remove the device after shutdown. A content library VM + template (`ovf` unset or `false`) can keep the vTPM. + + + + ### Create Configuration **Optional**: @@ -1973,6 +1994,7 @@ Clone the default **Read-Only** vSphere role and add the following privileges: | ... | Update library Item | `ContentLibrary.UpdateLibraryItem` | | Cryptographic Operations | Direct access | `Cryptographer.Access` | | ... | Encrypt | `Cryptographer.Encrypt` | +| ... | Decrypt | `Cryptographer.Decrypt` | | Datastore | Allocate space | `Datastore.AllocateSpace` | | ... | Browse datastore | `Datastore.Browse` | | ... | Low level file operations | `Datastore.FileManagement` | diff --git a/builder/vsphere/clone/builder.go b/builder/vsphere/clone/builder.go index a75a45a0..dba6742f 100644 --- a/builder/vsphere/clone/builder.go +++ b/builder/vsphere/clone/builder.go @@ -187,6 +187,9 @@ func (b *Builder) Run(ctx context.Context, ui packersdk.Ui, hook packersdk.Hook) Config: &b.config.ReattachCDRomConfig, CDRomConfig: &b.config.CDRomConfig, }, + &common.StepRemoveVTPM{ + Config: &b.config.RemoveVTPMConfig, + }, &common.StepCreateSnapshot{ CreateSnapshot: b.config.CreateSnapshot, SnapshotName: b.config.SnapshotName, diff --git a/builder/vsphere/clone/builder_acc_test.go b/builder/vsphere/clone/builder_acc_test.go index d8cd80ab..e72c0703 100644 --- a/builder/vsphere/clone/builder_acc_test.go +++ b/builder/vsphere/clone/builder_acc_test.go @@ -343,6 +343,21 @@ func teardownVM(vmName string) error { return acceptance.CleanupVm(d, vmName) } +func teardownContentLibraryItem(libraryName, itemName string) error { + d, err := acceptance.TestConn() + if err != nil { + return fmt.Errorf("cannot connect %v", err) + } + item, err := d.ResolveContentLibraryItem(libraryName, itemName) + if err != nil { + if strings.Contains(err.Error(), "not found") { + return nil + } + return err + } + return d.DeleteContentLibraryItem(item.ID) +} + // --------------------------------------------------------------------------- // Matrix A — Template Source // --------------------------------------------------------------------------- @@ -692,3 +707,62 @@ func checkMatrixI(name string, acc env.AccConfig, policies []string) error { } return acceptance.CheckStoragePolicyDiskPlacements(d, vm, policies) } + +// --------------------------------------------------------------------------- +// Matrix J — vTPM add and remove with content library OVF +// --------------------------------------------------------------------------- + +func TestAccCloneBuilder_MatrixJ(t *testing.T) { + acceptance.RequireAcceptance(t) + acceptance.RequireKeyProvider(t) + acc := env.AccFromEnv() + config := cloneExampleConfig() + config["firmware"] = "efi" + config["vTPM"] = true + config["remove_vtpm"] = true + vmName := config["vm_name"].(string) + clItemName := vmName + "-ovf-template" + config["content_library_destination"] = map[string]any{ + "library": acc.ContentLibrary, + "name": clItemName, + "ovf": true, + } + + testCase := &acctest.PluginTestCase{ + Name: "vsphere-clone-matrix-j", + Template: acceptance.RenderConfig("vsphere-clone", config), + Teardown: func() error { + _ = teardownVM(vmName) + return teardownContentLibraryItem(acc.ContentLibrary, clItemName) + }, + Check: func(buildCommand *exec.Cmd, logfile string) error { + if err := checkBuildSucceeded(buildCommand, logfile); err != nil { + return err + } + return checkMatrixJ(vmName, acc, acc.ContentLibrary, clItemName) + }, + } + acctest.TestPlugin(t, testCase) +} + +func checkMatrixJ(name string, acc env.AccConfig, libraryName, itemName string) error { + d, vm, parent, rp, err := findVM(name) + if err != nil { + return err + } + if err := checkFolderAndResourcePool(d, parent, rp, acc); err != nil { + return err + } + if err := acceptance.CheckNoVTPM(vm); err != nil { + return err + } + + item, err := d.ResolveContentLibraryItem(libraryName, itemName) + if err != nil { + return fmt.Errorf("expected content library OVF item: %v", err) + } + if !strings.EqualFold(item.Type, "ovf") { + return fmt.Errorf("unexpected content library item type %q, want ovf", item.Type) + } + return nil +} diff --git a/builder/vsphere/clone/config.go b/builder/vsphere/clone/config.go index 5878d67e..0dac2fac 100644 --- a/builder/vsphere/clone/config.go +++ b/builder/vsphere/clone/config.go @@ -32,6 +32,7 @@ type Config struct { common.RemoveCDRomConfig `mapstructure:",squash"` common.ReattachCDRomConfig `mapstructure:",squash"` common.RemoveNetworkAdapterConfig `mapstructure:",squash"` + common.RemoveVTPMConfig `mapstructure:",squash"` common.FloppyConfig `mapstructure:",squash"` common.RunConfig `mapstructure:",squash"` common.BootConfig `mapstructure:",squash"` @@ -120,6 +121,10 @@ func (c *Config) Prepare(raws ...any) ([]string, error) { warnings = append(warnings, customizeWarnings...) } + exportOVF := c.Export != nil + contentLibraryOVF := c.ContentLibraryDestinationConfig != nil && c.ContentLibraryDestinationConfig.Ovf + warnings = append(warnings, c.RemoveVTPMConfig.Prepare(c.VTPMEnabled, exportOVF, contentLibraryOVF)...) + if len(errs.Errors) > 0 { return nil, errs } diff --git a/builder/vsphere/clone/config.hcl2spec.go b/builder/vsphere/clone/config.hcl2spec.go index de898ae7..6113b285 100644 --- a/builder/vsphere/clone/config.hcl2spec.go +++ b/builder/vsphere/clone/config.hcl2spec.go @@ -84,6 +84,7 @@ type FlatConfig struct { RemoveCdrom *bool `mapstructure:"remove_cdrom" cty:"remove_cdrom" hcl:"remove_cdrom"` ReattachCDRom *int `mapstructure:"reattach_cdroms" cty:"reattach_cdroms" hcl:"reattach_cdroms"` RemoveNetworkAdapter *bool `mapstructure:"remove_network_adapter" cty:"remove_network_adapter" hcl:"remove_network_adapter"` + RemoveVTPM *bool `mapstructure:"remove_vtpm" cty:"remove_vtpm" hcl:"remove_vtpm"` FloppyIMGPath *string `mapstructure:"floppy_img_path" cty:"floppy_img_path" hcl:"floppy_img_path"` FloppyFiles []string `mapstructure:"floppy_files" cty:"floppy_files" hcl:"floppy_files"` FloppyDirectories []string `mapstructure:"floppy_dirs" cty:"floppy_dirs" hcl:"floppy_dirs"` @@ -245,6 +246,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec { "remove_cdrom": &hcldec.AttrSpec{Name: "remove_cdrom", Type: cty.Bool, Required: false}, "reattach_cdroms": &hcldec.AttrSpec{Name: "reattach_cdroms", Type: cty.Number, Required: false}, "remove_network_adapter": &hcldec.AttrSpec{Name: "remove_network_adapter", Type: cty.Bool, Required: false}, + "remove_vtpm": &hcldec.AttrSpec{Name: "remove_vtpm", Type: cty.Bool, Required: false}, "floppy_img_path": &hcldec.AttrSpec{Name: "floppy_img_path", Type: cty.String, Required: false}, "floppy_files": &hcldec.AttrSpec{Name: "floppy_files", Type: cty.List(cty.String), Required: false}, "floppy_dirs": &hcldec.AttrSpec{Name: "floppy_dirs", Type: cty.List(cty.String), Required: false}, diff --git a/builder/vsphere/common/step_hardware.go b/builder/vsphere/common/step_hardware.go index 194dacfd..d396be51 100644 --- a/builder/vsphere/common/step_hardware.go +++ b/builder/vsphere/common/step_hardware.go @@ -113,6 +113,13 @@ type HardwareConfig struct { BootDelay int64 `mapstructure:"boot_delay"` // Enable virtual trusted platform module (TPM) device for the virtual // machine. Defaults to `false`. + // + // -> **Note:** A virtual machine with a vTPM cannot be exported as OVF/OVA + // (`export`) or imported to a content library as an OVF template + // (`content_library_destination` with `ovf` set to `true`). Set + // [`remove_vtpm`](#remove_vtpm) to `true` to remove the device after + // shutdown. A content library VM template (`ovf` unset or `false`) can keep + // the vTPM. VTPMEnabled bool `mapstructure:"vTPM"` // The virtual precision clock device for the virtual machine. // Defaults to `none`. diff --git a/builder/vsphere/common/step_remove_vtpm.go b/builder/vsphere/common/step_remove_vtpm.go new file mode 100644 index 00000000..898bed06 --- /dev/null +++ b/builder/vsphere/common/step_remove_vtpm.go @@ -0,0 +1,78 @@ +// © Broadcom. All Rights Reserved. +// The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. +// SPDX-License-Identifier: MPL-2.0 + +//go:generate packer-sdc struct-markdown +//go:generate packer-sdc mapstructure-to-hcl2 -type RemoveVTPMConfig + +package common + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/packer-plugin-sdk/multistep" + packersdk "github.com/hashicorp/packer-plugin-sdk/packer" + "github.com/vmware/packer-plugin-vsphere/builder/vsphere/driver" +) + +type RemoveVTPMConfig struct { + // Remove the virtual trusted platform module (vTPM) device from the virtual + // machine after shutdown. Defaults to `false`. + // + // -> **Note:** A virtual machine with a vTPM cannot be exported as OVF/OVA + // (`export`) or imported to a content library as an OVF template + // (`content_library_destination` with `ovf` set to `true`). Set this option + // to `true` to remove the device after shutdown. A content library VM + // template (`ovf` unset or `false`) can keep the vTPM. + RemoveVTPM bool `mapstructure:"remove_vtpm"` +} + +// Prepare returns warnings when a vTPM would block OVF/OVA export or a +// content library OVF template import. +func (c *RemoveVTPMConfig) Prepare(vtpmEnabled, exportOVF, contentLibraryOVF bool) []string { + if !vtpmEnabled || c.RemoveVTPM { + return nil + } + + var ops []string + if exportOVF { + ops = append(ops, "OVF/OVA export is configured") + } + if contentLibraryOVF { + ops = append(ops, "content library OVF template import is configured") + } + if len(ops) == 0 { + return nil + } + + return []string{fmt.Sprintf("vTPM is enabled and %s; this will fail unless 'remove_vtpm' is true", strings.Join(ops, " and "))} +} + +type StepRemoveVTPM struct { + Config *RemoveVTPMConfig +} + +func (s *StepRemoveVTPM) Run(_ context.Context, state multistep.StateBag) multistep.StepAction { + if !s.Config.RemoveVTPM { + return multistep.ActionContinue + } + + ui := state.Get("ui").(packersdk.Ui) + vm := state.Get("vm").(driver.VirtualMachine) + + ui.Say("Removing vTPM...") + err := vm.RemoveVTPM() + + if err != nil { + state.Put("error", fmt.Errorf("error removing vTPM: %v", err)) + return multistep.ActionHalt + } + + return multistep.ActionContinue +} + +func (s *StepRemoveVTPM) Cleanup(state multistep.StateBag) { + // no cleanup +} diff --git a/builder/vsphere/common/step_remove_vtpm.hcl2spec.go b/builder/vsphere/common/step_remove_vtpm.hcl2spec.go new file mode 100644 index 00000000..f9cf5b4b --- /dev/null +++ b/builder/vsphere/common/step_remove_vtpm.hcl2spec.go @@ -0,0 +1,31 @@ +// Code generated by "packer-sdc mapstructure-to-hcl2"; DO NOT EDIT. + +package common + +import ( + "github.com/hashicorp/hcl/v2/hcldec" + "github.com/zclconf/go-cty/cty" +) + +// FlatRemoveVTPMConfig is an auto-generated flat version of RemoveVTPMConfig. +// Where the contents of a field with a `mapstructure:,squash` tag are bubbled up. +type FlatRemoveVTPMConfig struct { + RemoveVTPM *bool `mapstructure:"remove_vtpm" cty:"remove_vtpm" hcl:"remove_vtpm"` +} + +// FlatMapstructure returns a new FlatRemoveVTPMConfig. +// FlatRemoveVTPMConfig is an auto-generated flat version of RemoveVTPMConfig. +// Where the contents a fields with a `mapstructure:,squash` tag are bubbled up. +func (*RemoveVTPMConfig) FlatMapstructure() interface{ HCL2Spec() map[string]hcldec.Spec } { + return new(FlatRemoveVTPMConfig) +} + +// HCL2Spec returns the hcl spec of a RemoveVTPMConfig. +// This spec is used by HCL to read the fields of RemoveVTPMConfig. +// The decoded values from this spec will then be applied to a FlatRemoveVTPMConfig. +func (*FlatRemoveVTPMConfig) HCL2Spec() map[string]hcldec.Spec { + s := map[string]hcldec.Spec{ + "remove_vtpm": &hcldec.AttrSpec{Name: "remove_vtpm", Type: cty.Bool, Required: false}, + } + return s +} diff --git a/builder/vsphere/common/step_remove_vtpm_test.go b/builder/vsphere/common/step_remove_vtpm_test.go new file mode 100644 index 00000000..d9bcba1c --- /dev/null +++ b/builder/vsphere/common/step_remove_vtpm_test.go @@ -0,0 +1,164 @@ +// © Broadcom. All Rights Reserved. +// The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. +// SPDX-License-Identifier: MPL-2.0 + +package common + +import ( + "context" + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/hashicorp/packer-plugin-sdk/multistep" + "github.com/vmware/packer-plugin-vsphere/builder/vsphere/driver" +) + +func TestStepRemoveVTPM_Run(t *testing.T) { + tc := []struct { + name string + step *StepRemoveVTPM + expectedAction multistep.StepAction + vmMock *driver.VirtualMachineMock + expectedVmMock *driver.VirtualMachineMock + errMessage string + }{ + { + name: "Skip when remove_vtpm is false.", + step: &StepRemoveVTPM{ + Config: &RemoveVTPMConfig{ + RemoveVTPM: false, + }, + }, + expectedAction: multistep.ActionContinue, + vmMock: new(driver.VirtualMachineMock), + expectedVmMock: new(driver.VirtualMachineMock), + }, + { + name: "Successfully remove vTPM.", + step: &StepRemoveVTPM{ + Config: &RemoveVTPMConfig{ + RemoveVTPM: true, + }, + }, + expectedAction: multistep.ActionContinue, + vmMock: new(driver.VirtualMachineMock), + expectedVmMock: &driver.VirtualMachineMock{ + RemoveVTPMCalled: true, + }, + }, + { + name: "Fail to remove vTPM.", + step: &StepRemoveVTPM{ + Config: &RemoveVTPMConfig{ + RemoveVTPM: true, + }, + }, + expectedAction: multistep.ActionHalt, + vmMock: &driver.VirtualMachineMock{ + RemoveVTPMErr: fmt.Errorf("failed to remove vTPM"), + }, + expectedVmMock: &driver.VirtualMachineMock{ + RemoveVTPMCalled: true, + }, + errMessage: "error removing vTPM: failed to remove vTPM", + }, + } + + for _, c := range tc { + t.Run(c.name, func(t *testing.T) { + state := basicStateBag(nil) + state.Put("vm", c.vmMock) + + if action := c.step.Run(context.Background(), state); action != c.expectedAction { + t.Fatalf("unexpected action: expected '%#v', but returned '%#v'", c.expectedAction, action) + } + err, ok := state.Get("error").(error) + if ok { + if err.Error() != c.errMessage { + t.Fatalf("unexpected error: expected '%s', but returned '%s'", c.errMessage, err) + } + } else if c.errMessage != "" { + t.Fatalf("unexpected success, expected error: '%s'", c.errMessage) + } + + if diff := cmp.Diff(c.vmMock, c.expectedVmMock, + cmpopts.IgnoreInterfaces(struct{ error }{})); diff != "" { + t.Fatalf("unexpected '%s' calls: %s", "VirtualMachine", diff) + } + }) + } +} + +func TestRemoveVTPMConfig_Prepare(t *testing.T) { + const ( + warnExport = "vTPM is enabled and OVF/OVA export is configured; this will fail unless 'remove_vtpm' is true" + warnCL = "vTPM is enabled and content library OVF template import is configured; this will fail unless 'remove_vtpm' is true" + warnBoth = "vTPM is enabled and OVF/OVA export is configured and content library OVF template import is configured; this will fail unless 'remove_vtpm' is true" + ) + tc := []struct { + name string + config RemoveVTPMConfig + vtpmEnabled bool + exportOVF bool + contentLibraryOVF bool + wantWarning string + }{ + { + name: "No warning when vTPM is disabled.", + vtpmEnabled: false, + exportOVF: true, + }, + { + name: "No warning when neither OVF path is configured.", + vtpmEnabled: true, + }, + { + name: "No warning when remove_vtpm is true.", + config: RemoveVTPMConfig{ + RemoveVTPM: true, + }, + vtpmEnabled: true, + exportOVF: true, + contentLibraryOVF: true, + }, + { + name: "Warn for OVF/OVA export.", + vtpmEnabled: true, + exportOVF: true, + wantWarning: warnExport, + }, + { + name: "Warn for content library OVF template.", + vtpmEnabled: true, + contentLibraryOVF: true, + wantWarning: warnCL, + }, + { + name: "Warn for export and content library OVF template.", + vtpmEnabled: true, + exportOVF: true, + contentLibraryOVF: true, + wantWarning: warnBoth, + }, + } + + for _, c := range tc { + t.Run(c.name, func(t *testing.T) { + warnings := c.config.Prepare(c.vtpmEnabled, c.exportOVF, c.contentLibraryOVF) + if c.wantWarning == "" { + if len(warnings) != 0 { + t.Fatalf("expected no warnings, got %#v", warnings) + } + return + } + if len(warnings) != 1 { + t.Fatalf("expected 1 warning, got %#v", warnings) + } + if warnings[0] != c.wantWarning { + t.Fatalf("unexpected warning: %s", warnings[0]) + } + }) + } +} diff --git a/builder/vsphere/driver/vm.go b/builder/vsphere/driver/vm.go index 5c858378..0208b479 100644 --- a/builder/vsphere/driver/vm.go +++ b/builder/vsphere/driver/vm.go @@ -77,6 +77,7 @@ type VirtualMachine interface { FindSATAController() (*types.VirtualAHCIController, error) RemoveNetworkAdapters() error + RemoveVTPM() error Reference() types.ManagedObjectReference } @@ -733,14 +734,13 @@ func (vm *VirtualMachineDriver) Configure(config *HardwareConfig) error { if err != nil { return err } - TPMs := devices.SelectByType((*types.VirtualTPM)(nil)) - hasTPM := len(TPMs) > 0 + hasTPM := len(devices.SelectByType((*types.VirtualTPM)(nil))) > 0 if config.VTPMEnabled != hasTPM { if !hasTPM { device := &types.VirtualTPM{} err = vm.addDevice(device) } else { - err = vm.RemoveDevice(false, TPMs...) + err = vm.RemoveVTPM() } } if err != nil { @@ -1641,3 +1641,23 @@ func (vm *VirtualMachineDriver) RemoveNetworkAdapters() error { return nil } + +// RemoveVTPM removes the virtual trusted platform module (vTPM) device from +// the virtual machine, if present. +func (vm *VirtualMachineDriver) RemoveVTPM() error { + devices, err := vm.Devices() + if err != nil { + return fmt.Errorf("error retrieving devices: %s", err) + } + + tpms := devices.SelectByType((*types.VirtualTPM)(nil)) + if len(tpms) == 0 { + return nil + } + + if err = vm.RemoveDevice(false, tpms...); err != nil { + return fmt.Errorf("error removing vTPM: %s", err) + } + + return nil +} diff --git a/builder/vsphere/driver/vm_mock.go b/builder/vsphere/driver/vm_mock.go index a641c472..6426fead 100644 --- a/builder/vsphere/driver/vm_mock.go +++ b/builder/vsphere/driver/vm_mock.go @@ -85,6 +85,9 @@ type VirtualMachineMock struct { NetworkAdaptersList object.VirtualDeviceList RemoveNetworkAdaptersErr error + RemoveVTPMCalled bool + RemoveVTPMErr error + CloneCalled bool CloneConfig *CloneConfig CloneError error @@ -343,6 +346,11 @@ func (vm *VirtualMachineMock) RemoveNetworkAdapters() error { return vm.RemoveNetworkAdaptersErr } +func (vm *VirtualMachineMock) RemoveVTPM() error { + vm.RemoveVTPMCalled = true + return vm.RemoveVTPMErr +} + func (vm *VirtualMachineMock) Datacenter() *object.Datacenter { return nil } diff --git a/builder/vsphere/iso/builder.go b/builder/vsphere/iso/builder.go index 57b5bdde..e7ad4504 100644 --- a/builder/vsphere/iso/builder.go +++ b/builder/vsphere/iso/builder.go @@ -211,6 +211,9 @@ func (b *Builder) Run(ctx context.Context, ui packersdk.Ui, hook packersdk.Hook) &common.StepRemoveNetworkAdapter{ Config: &b.config.RemoveNetworkAdapterConfig, }, + &common.StepRemoveVTPM{ + Config: &b.config.RemoveVTPMConfig, + }, &common.StepCreateSnapshot{ CreateSnapshot: b.config.CreateSnapshot, SnapshotName: b.config.SnapshotName, diff --git a/builder/vsphere/iso/builder_acc_test.go b/builder/vsphere/iso/builder_acc_test.go index ccda4c0e..7cd55cd9 100644 --- a/builder/vsphere/iso/builder_acc_test.go +++ b/builder/vsphere/iso/builder_acc_test.go @@ -655,3 +655,98 @@ func checkMatrixE(name string, policies []string) error { } return acceptance.CheckStoragePolicyDiskPlacements(d, vm, policies) } + +// --------------------------------------------------------------------------- +// Matrix F — vTPM add and remove with OVF export and content library +// --------------------------------------------------------------------------- + +func TestAccISOBuilder_MatrixF(t *testing.T) { + acceptance.RequireAcceptance(t) + acceptance.RequireKeyProvider(t) + acc := env.AccFromEnv() + + config := alpineExampleConfig() + alpineMatrixGuest(config) + config["guest_os_type"] = "ubuntu64Guest" + config["vTPM"] = true + config["remove_vtpm"] = true + + vmName := config["vm_name"].(string) + clItemName := vmName + "-ovf-template" + exportDir := filepath.Join(os.TempDir(), vmName+"-vtpm-export") + config["content_library_destination"] = map[string]any{ + "library": acc.ContentLibrary, + "name": clItemName, + "ovf": true, + } + config["export"] = map[string]any{ + "force": true, + "output_directory": exportDir, + "output_format": "ovf", + } + + testCase := &acctest.PluginTestCase{ + Name: "vsphere-iso-matrix-f", + Template: acceptance.RenderConfig("vsphere-iso", config), + Teardown: func() error { + _ = os.RemoveAll(exportDir) + _ = teardownVM(vmName) + return teardownContentLibraryItem(acc.ContentLibrary, clItemName) + }, + Check: func(buildCommand *exec.Cmd, logfile string) error { + if err := checkBuildSucceeded(buildCommand, logfile); err != nil { + return err + } + return checkMatrixF(vmName, acc, acc.ContentLibrary, clItemName, exportDir) + }, + } + acctest.TestPlugin(t, testCase) +} + +func checkMatrixF(name string, acc env.AccConfig, libraryName, itemName, exportDir string) error { + d, err := acceptance.TestConn() + if err != nil { + return fmt.Errorf("cannot connect %v", err) + } + vm, err := d.FindVM(name) + if err != nil { + return fmt.Errorf("cannot find VM: %v", err) + } + + vmInfo, err := vm.Info("name", "parent", "resourcePool", "config") + if err != nil { + return fmt.Errorf("cannot read VM properties: %v", err) + } + if err := checkFolderAndResourcePool(d, vmInfo.Parent, vmInfo.ResourcePool, acc, true); err != nil { + return err + } + if vmInfo.Config == nil || vmInfo.Config.Firmware != "efi" { + got := "" + if vmInfo.Config != nil { + got = vmInfo.Config.Firmware + } + return fmt.Errorf("unexpected firmware: expected 'efi', got %q", got) + } + if err := acceptance.CheckNoVTPM(vm); err != nil { + return err + } + + item, err := d.ResolveContentLibraryItem(libraryName, itemName) + if err != nil { + return fmt.Errorf("expected content library OVF item: %v", err) + } + if !strings.EqualFold(item.Type, "ovf") { + return fmt.Errorf("unexpected content library item type %q, want ovf", item.Type) + } + + ovfPath := filepath.Join(exportDir, name+".ovf") + if _, err := os.Stat(ovfPath); err != nil { + entries, _ := os.ReadDir(exportDir) + names := make([]string, 0, len(entries)) + for _, e := range entries { + names = append(names, e.Name()) + } + return fmt.Errorf("expected export OVF at %s (dir contents: %v): %v", ovfPath, names, err) + } + return nil +} diff --git a/builder/vsphere/iso/config.go b/builder/vsphere/iso/config.go index 868b5830..ed9c9a7c 100644 --- a/builder/vsphere/iso/config.go +++ b/builder/vsphere/iso/config.go @@ -33,6 +33,7 @@ type Config struct { common.RemoveCDRomConfig `mapstructure:",squash"` common.ReattachCDRomConfig `mapstructure:",squash"` common.RemoveNetworkAdapterConfig `mapstructure:",squash"` + common.RemoveVTPMConfig `mapstructure:",squash"` common.FloppyConfig `mapstructure:",squash"` common.RunConfig `mapstructure:",squash"` common.BootConfig `mapstructure:",squash"` @@ -142,6 +143,10 @@ func (c *Config) Prepare(raws ...any) ([]string, error) { errs = packersdk.MultiErrorAppend(errs, c.ContentLibraryDestinationConfig.Prepare(&c.LocationConfig)...) } + exportOVF := c.Export != nil + contentLibraryOVF := c.ContentLibraryDestinationConfig != nil && c.ContentLibraryDestinationConfig.Ovf + warnings = append(warnings, c.RemoveVTPMConfig.Prepare(c.VTPMEnabled, exportOVF, contentLibraryOVF)...) + if len(errs.Errors) > 0 { return warnings, errs } diff --git a/builder/vsphere/iso/config.hcl2spec.go b/builder/vsphere/iso/config.hcl2spec.go index c400d754..d7385e0c 100644 --- a/builder/vsphere/iso/config.hcl2spec.go +++ b/builder/vsphere/iso/config.hcl2spec.go @@ -85,6 +85,7 @@ type FlatConfig struct { RemoveCdrom *bool `mapstructure:"remove_cdrom" cty:"remove_cdrom" hcl:"remove_cdrom"` ReattachCDRom *int `mapstructure:"reattach_cdroms" cty:"reattach_cdroms" hcl:"reattach_cdroms"` RemoveNetworkAdapter *bool `mapstructure:"remove_network_adapter" cty:"remove_network_adapter" hcl:"remove_network_adapter"` + RemoveVTPM *bool `mapstructure:"remove_vtpm" cty:"remove_vtpm" hcl:"remove_vtpm"` FloppyIMGPath *string `mapstructure:"floppy_img_path" cty:"floppy_img_path" hcl:"floppy_img_path"` FloppyFiles []string `mapstructure:"floppy_files" cty:"floppy_files" hcl:"floppy_files"` FloppyDirectories []string `mapstructure:"floppy_dirs" cty:"floppy_dirs" hcl:"floppy_dirs"` @@ -252,6 +253,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec { "remove_cdrom": &hcldec.AttrSpec{Name: "remove_cdrom", Type: cty.Bool, Required: false}, "reattach_cdroms": &hcldec.AttrSpec{Name: "reattach_cdroms", Type: cty.Number, Required: false}, "remove_network_adapter": &hcldec.AttrSpec{Name: "remove_network_adapter", Type: cty.Bool, Required: false}, + "remove_vtpm": &hcldec.AttrSpec{Name: "remove_vtpm", Type: cty.Bool, Required: false}, "floppy_img_path": &hcldec.AttrSpec{Name: "floppy_img_path", Type: cty.String, Required: false}, "floppy_files": &hcldec.AttrSpec{Name: "floppy_files", Type: cty.List(cty.String), Required: false}, "floppy_dirs": &hcldec.AttrSpec{Name: "floppy_dirs", Type: cty.List(cty.String), Required: false}, diff --git a/docs-partials/builder/vsphere/common/HardwareConfig-not-required.mdx b/docs-partials/builder/vsphere/common/HardwareConfig-not-required.mdx index 3cc85e6b..5d0a84a2 100644 --- a/docs-partials/builder/vsphere/common/HardwareConfig-not-required.mdx +++ b/docs-partials/builder/vsphere/common/HardwareConfig-not-required.mdx @@ -53,6 +53,13 @@ - `vTPM` (bool) - Enable virtual trusted platform module (TPM) device for the virtual machine. Defaults to `false`. + + -> **Note:** A virtual machine with a vTPM cannot be exported as OVF/OVA + (`export`) or imported to a content library as an OVF template + (`content_library_destination` with `ovf` set to `true`). Set + [`remove_vtpm`](#remove_vtpm) to `true` to remove the device after + shutdown. A content library VM template (`ovf` unset or `false`) can keep + the vTPM. - `precision_clock` (string) - The virtual precision clock device for the virtual machine. Defaults to `none`. diff --git a/docs-partials/builder/vsphere/common/RemoveVTPMConfig-not-required.mdx b/docs-partials/builder/vsphere/common/RemoveVTPMConfig-not-required.mdx new file mode 100644 index 00000000..79dee2a5 --- /dev/null +++ b/docs-partials/builder/vsphere/common/RemoveVTPMConfig-not-required.mdx @@ -0,0 +1,12 @@ + + +- `remove_vtpm` (bool) - Remove the virtual trusted platform module (vTPM) device from the virtual + machine after shutdown. Defaults to `false`. + + -> **Note:** A virtual machine with a vTPM cannot be exported as OVF/OVA + (`export`) or imported to a content library as an OVF template + (`content_library_destination` with `ovf` set to `true`). Set this option + to `true` to remove the device after shutdown. A content library VM + template (`ovf` unset or `false`) can keep the vTPM. + + diff --git a/docs/builders/vsphere-clone.mdx b/docs/builders/vsphere-clone.mdx index d8d9a176..40efe6d3 100644 --- a/docs/builders/vsphere-clone.mdx +++ b/docs/builders/vsphere-clone.mdx @@ -636,6 +636,8 @@ JSON Example: @include 'builder/vsphere/common/HardwareConfig-not-required.mdx' +@include 'builder/vsphere/common/RemoveVTPMConfig-not-required.mdx' + ### Location Configuration **Optional:** @@ -880,6 +882,7 @@ Clone the default **Read-Only** vSphere role and add the following privileges: | ... | Download files | `ContentLibrary.DownloadSession` | | Cryptographic Operations | Direct access | `Cryptographer.Access` | | ... | Encrypt | `Cryptographer.Encrypt` | +| ... | Decrypt | `Cryptographer.Decrypt` | | Datastore | Allocate space | `Datastore.AllocateSpace` | | ... | Browse datastore | `Datastore.Browse` | | ... | Low level file operations | `Datastore.FileManagement` | diff --git a/docs/builders/vsphere-iso.mdx b/docs/builders/vsphere-iso.mdx index 677b639c..535beffe 100644 --- a/docs/builders/vsphere-iso.mdx +++ b/docs/builders/vsphere-iso.mdx @@ -75,6 +75,8 @@ their respective End of General Support dates. For detailed information, refer t @include 'builder/vsphere/common/HardwareConfig-not-required.mdx' +@include 'builder/vsphere/common/RemoveVTPMConfig-not-required.mdx' + ### Create Configuration **Optional**: @@ -525,6 +527,7 @@ Clone the default **Read-Only** vSphere role and add the following privileges: | ... | Update library Item | `ContentLibrary.UpdateLibraryItem` | | Cryptographic Operations | Direct access | `Cryptographer.Access` | | ... | Encrypt | `Cryptographer.Encrypt` | +| ... | Decrypt | `Cryptographer.Decrypt` | | Datastore | Allocate space | `Datastore.AllocateSpace` | | ... | Browse datastore | `Datastore.Browse` | | ... | Low level file operations | `Datastore.FileManagement` | diff --git a/testing/acceptance/key_provider.go b/testing/acceptance/key_provider.go new file mode 100644 index 00000000..e3fc5941 --- /dev/null +++ b/testing/acceptance/key_provider.go @@ -0,0 +1,41 @@ +// © Broadcom. All Rights Reserved. +// The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. +// SPDX-License-Identifier: MPL-2.0 + +package acceptance + +import ( + "testing" + + "github.com/vmware/govmomi/crypto" + "github.com/vmware/packer-plugin-vsphere/builder/vsphere/driver" +) + +// RequireKeyProvider skips the test unless vCenter has a Native Key Provider +// or KMIP cluster. Adding a vTPM encrypts VM home files and requires one. +func RequireKeyProvider(t *testing.T) { + t.Helper() + + d, err := TestConn() + if err != nil { + t.Fatalf("cannot connect: %v", err) + } + + vc, ok := d.(*driver.VCenterDriver) + if !ok { + t.Fatalf("driver does not support key provider queries") + } + + m, err := crypto.GetManagerKmip(vc.VimClient) + if err != nil { + t.Skipf("vTPM ACC skipped: CryptoManager is not available (%v); configure Native Key Provider or KMIP", err) + } + + clusters, err := m.ListKmipServers(vc.Ctx, nil) + if err != nil { + t.Fatalf("list key providers: %v", err) + } + if len(clusters) == 0 { + t.Skip("vTPM ACC skipped: no Native Key Provider or KMIP cluster configured on vCenter") + } +} diff --git a/testing/acceptance/vtpm.go b/testing/acceptance/vtpm.go new file mode 100644 index 00000000..8e0992a3 --- /dev/null +++ b/testing/acceptance/vtpm.go @@ -0,0 +1,25 @@ +// © Broadcom. All Rights Reserved. +// The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. +// SPDX-License-Identifier: MPL-2.0 + +package acceptance + +import ( + "fmt" + + "github.com/vmware/govmomi/vim25/types" + "github.com/vmware/packer-plugin-vsphere/builder/vsphere/driver" +) + +// CheckNoVTPM asserts the virtual machine has no virtual TPM devices. +func CheckNoVTPM(vm driver.VirtualMachine) error { + devices, err := vm.Devices() + if err != nil { + return fmt.Errorf("cannot read devices: %v", err) + } + tpms := devices.SelectByType((*types.VirtualTPM)(nil)) + if len(tpms) != 0 { + return fmt.Errorf("expected remove_vtpm to leave zero vTPM devices, got %d", len(tpms)) + } + return nil +}