Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .web-docs/components/builder/vsphere-clone/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,10 @@ JSON Example:
- `disk_controller_index` (int) - The assigned disk controller for the disk.
Defaults to the first controller, `(0)`.

- `storage_policy` (string) - The name of the storage policy to apply to the disk. The storage policy
must already exist on the vCenter Server. If not specified, the default
storage policy of the target datastore is used.

<!-- End of code generated from the comments of the DiskConfig struct in builder/vsphere/common/storage_config.go; -->


Expand Down
4 changes: 4 additions & 0 deletions .web-docs/components/builder/vsphere-iso/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -877,6 +877,10 @@ JSON Example:
- `disk_controller_index` (int) - The assigned disk controller for the disk.
Defaults to the first controller, `(0)`.

- `storage_policy` (string) - The name of the storage policy to apply to the disk. The storage policy
must already exist on the vCenter Server. If not specified, the default
storage policy of the target datastore is used.

<!-- End of code generated from the comments of the DiskConfig struct in builder/vsphere/common/storage_config.go; -->


Expand Down
4 changes: 4 additions & 0 deletions builder/vsphere/common/storage_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,10 @@ type DiskConfig struct {
// The assigned disk controller for the disk.
// Defaults to the first controller, `(0)`.
DiskControllerIndex int `mapstructure:"disk_controller_index"`
// The name of the storage policy to apply to the disk. The storage policy
// must already exist on the vCenter Server. If not specified, the default
// storage policy of the target datastore is used.
StoragePolicyName string `mapstructure:"storage_policy"`
}

type StorageConfig struct {
Expand Down
10 changes: 6 additions & 4 deletions builder/vsphere/common/storage_config.hcl2spec.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

34 changes: 29 additions & 5 deletions builder/vsphere/driver/disk.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ type Disk struct {
DiskEagerlyScrub bool
DiskThinProvisioned bool
ControllerIndex int
// StoragePolicyID is the UUID of the vSphere storage policy to associate
// with this disk. Empty means no explicit policy; the datastore default applies.
StoragePolicyID string
}

type StorageConfig struct {
Expand All @@ -28,10 +31,12 @@ type StorageConfig struct {
// It creates a controller for each type specified in DiskControllerType and attaches
// virtual disks to the controllers. If DatastoreRefs is provided, each disk is placed
// on the corresponding datastore; otherwise, disks inherit the VM's datastore.
// When a disk has a StoragePolicyID, a VirtualMachineDefinedProfileSpec is attached
// to that disk's device config spec.
func (c *StorageConfig) AddStorageDevices(existingDevices object.VirtualDeviceList) ([]types.BaseVirtualDeviceConfigSpec, error) {
newDevices := object.VirtualDeviceList{}

var controllerDevices object.VirtualDeviceList
var controllers []types.BaseVirtualController

for _, controllerType := range c.DiskControllerType {
var device types.BaseVirtualDevice
var err error
Expand All @@ -47,14 +52,19 @@ func (c *StorageConfig) AddStorageDevices(existingDevices object.VirtualDeviceLi
return nil, err
}
existingDevices = append(existingDevices, device)
newDevices = append(newDevices, device)
controllerDevices = append(controllerDevices, device)
controller, err := existingDevices.FindDiskController(existingDevices.Name(device))
if err != nil {
return nil, err
}
controllers = append(controllers, controller)
}

allSpecs, err := controllerDevices.ConfigSpec(types.VirtualDeviceConfigSpecOperationAdd)
if err != nil {
return nil, err
}

for i, dc := range c.Storage {
backing := &types.VirtualDiskFlatVer2BackingInfo{
DiskMode: string(types.VirtualDiskModePersistent),
Expand All @@ -76,10 +86,24 @@ func (c *StorageConfig) AddStorageDevices(existingDevices object.VirtualDeviceLi

existingDevices.AssignController(disk, controllers[dc.ControllerIndex])
existingDevices = append(existingDevices, disk)
newDevices = append(newDevices, disk)

diskSpecs, err := object.VirtualDeviceList{disk}.ConfigSpec(types.VirtualDeviceConfigSpecOperationAdd)
if err != nil {
return nil, err
}
diskSpec := diskSpecs[0]
if dc.StoragePolicyID != "" {
vdcs := diskSpec.(*types.VirtualDeviceConfigSpec)
vdcs.Profile = []types.BaseVirtualMachineProfileSpec{
&types.VirtualMachineDefinedProfileSpec{
ProfileId: dc.StoragePolicyID,
},
}
}
allSpecs = append(allSpecs, diskSpec)
}

return newDevices.ConfigSpec(types.VirtualDeviceConfigSpecOperationAdd)
return allSpecs, nil
}

// findDisk scans a list of virtual devices and retrieves a single virtual disk.
Expand Down
45 changes: 45 additions & 0 deletions builder/vsphere/driver/disk_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"testing"

"github.com/vmware/govmomi/object"
"github.com/vmware/govmomi/vim25/types"
)

func TestAddStorageDevices(t *testing.T) {
Expand Down Expand Up @@ -51,3 +52,47 @@ func TestAddStorageDevices(t *testing.T) {
t.Fatalf("unexpected result: expected '3', but returned '%d'", len(storageConfigSpec))
}
}

// TestAddStorageDevices_WithStoragePolicy verifies that a disk with a
// StoragePolicyID gets a VirtualMachineDefinedProfileSpec in its config spec,
// while a disk without a policy gets no profile entry.
func TestAddStorageDevices_WithStoragePolicy(t *testing.T) {
const policyUUID = "aaaabbbb-cccc-dddd-eeee-ffffffffffff"

config := &StorageConfig{
DiskControllerType: []string{"pvscsi"},
Storage: []Disk{
{DiskSize: 10240, DiskThinProvisioned: true, ControllerIndex: 0, StoragePolicyID: policyUUID},
{DiskSize: 20480, DiskThinProvisioned: true, ControllerIndex: 0},
},
}

specs, err := config.AddStorageDevices(object.VirtualDeviceList{})
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
// 1 controller + 2 disks
if len(specs) != 3 {
t.Fatalf("expected 3 specs, got %d", len(specs))
}

// specs[0] = controller (no profile expected)
// specs[1] = first disk (policy set)
// specs[2] = second disk (no policy)
disk0spec := specs[1].(*types.VirtualDeviceConfigSpec)
if len(disk0spec.Profile) != 1 {
t.Fatalf("expected 1 profile on disk 0, got %d", len(disk0spec.Profile))
}
profileSpec, ok := disk0spec.Profile[0].(*types.VirtualMachineDefinedProfileSpec)
if !ok {
t.Fatal("expected VirtualMachineDefinedProfileSpec on disk 0")
}
if profileSpec.ProfileId != policyUUID {
t.Fatalf("expected profile ID %q, got %q", policyUUID, profileSpec.ProfileId)
}

disk1spec := specs[2].(*types.VirtualDeviceConfigSpec)
if len(disk1spec.Profile) != 0 {
t.Fatalf("expected no profile on disk 1, got %d", len(disk1spec.Profile))
}
}
25 changes: 25 additions & 0 deletions builder/vsphere/driver/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/vmware/govmomi"
"github.com/vmware/govmomi/find"
"github.com/vmware/govmomi/object"
"github.com/vmware/govmomi/pbm"
"github.com/vmware/govmomi/session"
"github.com/vmware/govmomi/vapi/library"
"github.com/vmware/govmomi/vapi/rest"
Expand Down Expand Up @@ -48,6 +49,11 @@ type Driver interface {
FindContentLibraryItem(libraryId string, name string) (*library.Item, error)
FindContentLibraryFileDatastorePath(isoPath string) (string, error)
UpdateContentLibraryItem(item *library.Item, name string, description string) error

// FindStoragePolicyID resolves a storage policy name to its profile UUID.
// Returns an error if the policy does not exist on vCenter.
FindStoragePolicyID(name string) (string, error)

Cleanup() (error, error)
}

Expand All @@ -58,6 +64,7 @@ type VCenterDriver struct {
RestClient *RestClient
Finder *find.Finder
Datacenter *object.Datacenter
pbmClient *pbm.Client
}

func NewVCenterDriver(ctx context.Context, client *govmomi.Client, vimClient *vim25.Client, user *url.Userinfo, finder *find.Finder, datacenter *object.Datacenter) *VCenterDriver {
Expand Down Expand Up @@ -131,6 +138,24 @@ func NewDriver(config *ConnectConfig) (Driver, error) {
return d, nil
}

// FindStoragePolicyID resolves a storage policy name to its profile UUID using
// the vSphere Policy-Based Management (PBM) API. The PBM client is initialized
// lazily on first use; it shares the existing VIM25 session.
func (d *VCenterDriver) FindStoragePolicyID(name string) (string, error) {
if d.pbmClient == nil {
c, err := pbm.NewClient(d.Ctx, d.VimClient)
if err != nil {
return "", fmt.Errorf("error initializing PBM client: %v", err)
}
d.pbmClient = c
}
id, err := d.pbmClient.ProfileIDByName(d.Ctx, name)
if err != nil {
return "", fmt.Errorf("storage policy %q not found: %v", name, err)
}
return id, nil
}

func (d *VCenterDriver) Cleanup() (error, error) {
return d.RestClient.client.Logout(d.Ctx), d.Client.SessionManager.Logout(d.Ctx)
}
Expand Down
11 changes: 11 additions & 0 deletions builder/vsphere/driver/driver_mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ type DriverMock struct {

FindVMCalled bool
FindVMName string

FindStoragePolicyIDCalled bool
FindStoragePolicyIDName string
FindStoragePolicyIDResult string
FindStoragePolicyIDErr error
}

func NewDriverMock() *DriverMock {
Expand Down Expand Up @@ -126,6 +131,12 @@ func (d *DriverMock) UpdateContentLibraryItem(item *library.Item, name string, d
return nil
}

func (d *DriverMock) FindStoragePolicyID(name string) (string, error) {
d.FindStoragePolicyIDCalled = true
d.FindStoragePolicyIDName = name
return d.FindStoragePolicyIDResult, d.FindStoragePolicyIDErr
}

func (d *DriverMock) Cleanup() (error, error) {
return nil, nil
}
15 changes: 15 additions & 0 deletions builder/vsphere/driver/vm.go
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,21 @@ func (d *VCenterDriver) CreateVM(config *CreateConfig) (VirtualMachine, error) {
}
createSpec.DeviceChange = append(createSpec.DeviceChange, storageConfigSpec...)

// vSphere requires VmProfile on the top-level config spec (which governs
// the VM home files: .vmx, .nvram, etc.) whenever any disk carries a
// per-disk storage policy profile. Use the first disk's policy for the
// VM home so the entire VM lives on compatible storage.
for _, disk := range config.StorageConfig.Storage {
if disk.StoragePolicyID != "" {
createSpec.VmProfile = []types.BaseVirtualMachineProfileSpec{
&types.VirtualMachineDefinedProfileSpec{
ProfileId: disk.StoragePolicyID,
},
}
break
}
}

devices, err = addNetwork(d, devices, config)
if err != nil {
return nil, err
Expand Down
50 changes: 50 additions & 0 deletions builder/vsphere/driver/vm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,56 @@ func TestVirtualMachineDriver_CreateVMWithMultipleDisks(t *testing.T) {
}
}

// TestVirtualMachineDriver_CreateVM_WithStoragePolicy verifies that CreateVM
// succeeds when a disk carries a StoragePolicyID, exercising the VmProfile
// code path in the VM config spec.
func TestVirtualMachineDriver_CreateVM_WithStoragePolicy(t *testing.T) {
sim, err := NewVCenterSimulator()
if err != nil {
t.Fatalf("unexpected error: '%s'", err)
}
defer sim.Close()

_, datastore := sim.ChooseSimulatorPreCreatedDatastore()

config := &CreateConfig{
Name: "mock-vm-with-policy",
Host: "DC0_H0",
Datastore: datastore.Name,
NICs: []NIC{
{
Network: "VM Network",
NetworkCard: "vmxnet3",
},
},
StorageConfig: StorageConfig{
DiskControllerType: []string{"pvscsi"},
Storage: []Disk{
{
DiskSize: 10240,
DiskThinProvisioned: true,
ControllerIndex: 0,
StoragePolicyID: "aaaabbbb-cccc-dddd-eeee-ffffffffffff",
},
{
DiskSize: 20480,
ControllerIndex: 0,
// No policy on this disk — VmProfile should still be set
// from the first disk's policy.
},
},
},
}

vm, err := sim.driver.CreateVM(config)
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
if vm == nil {
t.Fatal("expected a VM, got nil")
}
}

func TestVirtualMachineDriver_CloneWithPrimaryDiskResize(t *testing.T) {
sim, err := NewVCenterSimulator()
if err != nil {
Expand Down
13 changes: 11 additions & 2 deletions builder/vsphere/iso/step_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -204,12 +204,21 @@ func (s *StepCreateVM) Run(_ context.Context, state multistep.StateBag) multiste

var disks []driver.Disk
for _, disk := range s.Config.StorageConfig.Storage {
disks = append(disks, driver.Disk{
dd := driver.Disk{
DiskSize: disk.DiskSize,
DiskEagerlyScrub: disk.DiskEagerlyScrub,
DiskThinProvisioned: disk.DiskThinProvisioned,
ControllerIndex: disk.DiskControllerIndex,
})
}
if disk.StoragePolicyName != "" {
id, err := d.FindStoragePolicyID(disk.StoragePolicyName)
if err != nil {
state.Put("error", fmt.Errorf("error resolving storage policy %q: %v", disk.StoragePolicyName, err))
return multistep.ActionHalt
}
dd.StoragePolicyID = id
}
disks = append(disks, dd)
}

datastoreName := s.Location.Datastore
Expand Down
Loading