Skip to content

Commit bf66278

Browse files
committed
feat: add support for datastore clusters
Adds support for the use of datastore clusters in the applicable builders and post-processors. Signed-off-by: Ryan Johnson <ryan@tenthirtyam.org>
1 parent cc8ebd4 commit bf66278

27 files changed

+1086
-75
lines changed

.web-docs/components/builder/vsphere-clone/README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1036,7 +1036,15 @@ wget http://{{ .HTTPIP }}:{{ .HTTPPort }}/foo/bar/preseed.cfg
10361036
a nested path might resemble 'rp-packer/rp-linux-images'.
10371037

10381038
- `datastore` (string) - The datastore where the virtual machine is created.
1039-
Required if `host` is a cluster, or if `host` has multiple datastores.
1039+
Required if `host` is a cluster or if `host` has multiple datastores,
1040+
unless `datastore_cluster` is specified.
1041+
1042+
~> **Note:** Cannot be used with `datastore_cluster`.
1043+
1044+
- `datastore_cluster` (string) - The datastore cluster where the virtual machine is created.
1045+
When specified, Storage DRS will automatically select the optimal datastore.
1046+
1047+
~> **Note:** Cannot be used with `datastore`.
10401048

10411049
- `set_host_for_datastore_uploads` (bool) - The ESXI host used for uploading files to the datastore.
10421050
Defaults to `false`.

.web-docs/components/builder/vsphere-iso/README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,15 @@ wget http://{{ .HTTPIP }}:{{ .HTTPPort }}/foo/bar/preseed.cfg
156156
a nested path might resemble 'rp-packer/rp-linux-images'.
157157

158158
- `datastore` (string) - The datastore where the virtual machine is created.
159-
Required if `host` is a cluster, or if `host` has multiple datastores.
159+
Required if `host` is a cluster or if `host` has multiple datastores,
160+
unless `datastore_cluster` is specified.
161+
162+
~> **Note:** Cannot be used with `datastore_cluster`.
163+
164+
- `datastore_cluster` (string) - The datastore cluster where the virtual machine is created.
165+
When specified, Storage DRS will automatically select the optimal datastore.
166+
167+
~> **Note:** Cannot be used with `datastore`.
160168

161169
- `set_host_for_datastore_uploads` (bool) - The ESXI host used for uploading files to the datastore.
162170
Defaults to `false`.

.web-docs/components/post-processor/vsphere/README.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,6 @@ The following configuration options are available for the post-processor.
3030
- `datacenter` (string) - The name of the vSphere datacenter object to place the virtual machine.
3131
This is _not required_ if `resource_pool` is specified.
3232

33-
- `datastore` (string) - The name of the vSphere datastore to place the virtual machine.
34-
3533
- `host` (string) - The fully qualified domain name or IP address of the vCenter instance or ESX host.
3634

3735
- `password` (string) - The password to use to authenticate to the vSphere endpoint.
@@ -45,6 +43,13 @@ The following configuration options are available for the post-processor.
4543

4644
<!-- Code generated from the comments of the Config struct in post-processor/vsphere/post-processor.go; DO NOT EDIT MANUALLY -->
4745

46+
- `datastore` (string) - The name of the vSphere datastore to place the virtual machine.
47+
Mutually exclusive with `datastore_cluster`.
48+
49+
- `datastore_cluster` (string) - The name of the vSphere datastore cluster to place the virtual machine.
50+
When specified, Storage DRS will automatically select the optimal datastore.
51+
Mutually exclusive with `datastore`.
52+
4853
- `disk_mode` (string) - The disk format of the target virtual machine. One of `thin`, `thick`,
4954

5055
- `esxi_host` (string) - The fully qualified domain name or IP address of the ESX host to upload the
@@ -55,7 +60,8 @@ The following configuration options are available for the post-processor.
5560
- `options` ([]string) - Options to send to `ovftool` when uploading the virtual machine.
5661
Use `ovftool --help` to list all the options available.
5762

58-
- `overwrite` (bool) - Overwrite existing files. Defaults to `false`.
63+
- `overwrite` (bool) - Overwrite existing files.
64+
If `true`, forces overwrites of existing files. Defaults to `false`.
5965

6066
- `resource_pool` (string) - The name of the resource pool to place the virtual machine.
6167

builder/vsphere/clone/builder.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ func (b *Builder) Run(ctx context.Context, ui packersdk.Ui, hook packersdk.Hook)
4444
&common.StepConnect{
4545
Config: &b.config.ConnectConfig,
4646
},
47+
&common.StepResolveDatastore{
48+
Datastore: b.config.Datastore,
49+
DatastoreCluster: b.config.DatastoreCluster,
50+
DiskCount: len(b.config.StorageConfig.Storage),
51+
},
4752
&commonsteps.StepCreateCD{
4853
Files: b.config.CDFiles,
4954
Content: b.config.CDContent,

builder/vsphere/clone/config.hcl2spec.go

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

builder/vsphere/clone/step_clone.go

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ package clone
99
import (
1010
"context"
1111
"fmt"
12+
"log"
1213
"path"
1314
"strings"
1415

@@ -17,6 +18,7 @@ import (
1718
"github.com/hashicorp/packer-plugin-sdk/packerbuilderdata"
1819
"github.com/hashicorp/packer-plugin-vsphere/builder/vsphere/common"
1920
"github.com/hashicorp/packer-plugin-vsphere/builder/vsphere/driver"
21+
"github.com/vmware/govmomi/vim25/types"
2022
)
2123

2224
type vAppConfig struct {
@@ -159,13 +161,61 @@ func (s *StepCloneVM) Run(ctx context.Context, state multistep.StateBag) multist
159161
})
160162
}
161163

164+
datastoreName := s.Location.Datastore
165+
var primaryDatastore driver.Datastore
166+
if ds, ok := state.GetOk("datastore"); ok {
167+
primaryDatastore = ds.(driver.Datastore)
168+
datastoreName = primaryDatastore.Name()
169+
}
170+
171+
// If no datastore was resolved and no datastore was specified, return an error
172+
if datastoreName == "" && s.Location.DatastoreCluster == "" {
173+
state.Put("error", fmt.Errorf("no datastore specified and no datastore resolved from cluster"))
174+
return multistep.ActionHalt
175+
}
176+
177+
// Handle multi-disk placement when using a datastore cluster.
178+
var datastoreRefs []*types.ManagedObjectReference
179+
if s.Location.DatastoreCluster != "" && len(disks) > 1 {
180+
if vcDriver, ok := d.(*driver.VCenterDriver); ok {
181+
// Request Storage DRS recommendations for all disks at once for optimal placement.
182+
ui.Sayf("Requesting Storage DRS recommendations for %d disks...", len(disks))
183+
184+
diskDatastores, method, err := vcDriver.SelectDatastoresForDisks(s.Location.DatastoreCluster, disks)
185+
if err != nil {
186+
ui.Errorf("Warning: Failed to get Storage DRS recommendations: %s. Using primary datastore.", err)
187+
if primaryDatastore != nil {
188+
ref := primaryDatastore.Reference()
189+
for i := 0; i < len(disks); i++ {
190+
datastoreRefs = append(datastoreRefs, &ref)
191+
}
192+
}
193+
} else {
194+
// Use the first disk's datastore as the primary datastore.
195+
if len(diskDatastores) > 0 {
196+
datastoreName = diskDatastores[0].Name()
197+
}
198+
199+
for i, ds := range diskDatastores {
200+
ref := ds.Reference()
201+
if method == driver.SelectionMethodDRS {
202+
log.Printf("[INFO] Disk %d: Storage DRS selected datastore '%s'", i+1, ds.Name())
203+
} else {
204+
log.Printf("[INFO] Disk %d: Using first available datastore '%s'", i+1, ds.Name())
205+
}
206+
datastoreRefs = append(datastoreRefs, &ref)
207+
}
208+
}
209+
}
210+
}
211+
162212
vm, err := template.Clone(ctx, &driver.CloneConfig{
163213
Name: s.Location.VMName,
164214
Folder: s.Location.Folder,
165215
Cluster: s.Location.Cluster,
166216
Host: s.Location.Host,
167217
ResourcePool: s.Location.ResourcePool,
168-
Datastore: s.Location.Datastore,
218+
Datastore: datastoreName,
169219
LinkedClone: s.Config.LinkedClone,
170220
Network: s.Config.Network,
171221
MacAddress: strings.ToLower(s.Config.MacAddress),
@@ -175,6 +225,7 @@ func (s *StepCloneVM) Run(ctx context.Context, state multistep.StateBag) multist
175225
StorageConfig: driver.StorageConfig{
176226
DiskControllerType: s.Config.StorageConfig.DiskControllerType,
177227
Storage: disks,
228+
DatastoreRefs: datastoreRefs,
178229
},
179230
})
180231
if err != nil {

builder/vsphere/common/config_location.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,16 @@ type LocationConfig struct {
3535
// a nested path might resemble 'rp-packer/rp-linux-images'.
3636
ResourcePool string `mapstructure:"resource_pool"`
3737
// The datastore where the virtual machine is created.
38-
// Required if `host` is a cluster, or if `host` has multiple datastores.
38+
// Required if `host` is a cluster or if `host` has multiple datastores,
39+
// unless `datastore_cluster` is specified.
40+
//
41+
// ~> **Note:** Cannot be used with `datastore_cluster`.
3942
Datastore string `mapstructure:"datastore"`
43+
// The datastore cluster where the virtual machine is created.
44+
// When specified, Storage DRS will automatically select the optimal datastore.
45+
//
46+
// ~> **Note:** Cannot be used with `datastore`.
47+
DatastoreCluster string `mapstructure:"datastore_cluster"`
4048
// The ESXI host used for uploading files to the datastore.
4149
// Defaults to `false`.
4250
SetHostForDatastoreUploads bool `mapstructure:"set_host_for_datastore_uploads"`
@@ -52,7 +60,10 @@ func (c *LocationConfig) Prepare() []error {
5260
errs = append(errs, fmt.Errorf("'host' or 'cluster' is required"))
5361
}
5462

55-
// clean Folder path and remove leading slash as folders are relative within vsphere
63+
if c.Datastore != "" && c.DatastoreCluster != "" {
64+
errs = append(errs, fmt.Errorf("'datastore' and 'datastore_cluster' are mutually exclusive; specify only one"))
65+
}
66+
5667
c.Folder = path.Clean(c.Folder)
5768
c.Folder = strings.TrimLeft(c.Folder, "/")
5869

builder/vsphere/common/config_location.hcl2spec.go

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

builder/vsphere/common/step_add_floppy.go

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,18 @@ func (s *StepAddFloppy) Run(_ context.Context, state multistep.StateBag) multist
6363
if floppyPath, ok := state.GetOk("floppy_path"); ok {
6464
ui.Say("Uploading floppy image...")
6565

66-
ds, err := d.FindDatastore(s.Datastore, s.Host)
67-
if err != nil {
68-
state.Put("error", err)
69-
return multistep.ActionHalt
66+
var ds driver.Datastore
67+
var err error
68+
69+
// If a datastore was resolved (from datastore or datastore_cluster), use it.
70+
if resolvedDs, ok := state.GetOk("datastore"); ok {
71+
ds = resolvedDs.(driver.Datastore)
72+
} else {
73+
ds, err = d.FindDatastore(s.Datastore, s.Host)
74+
if err != nil {
75+
state.Put("error", err)
76+
return multistep.ActionHalt
77+
}
7078
}
7179
vmDir, err := vm.GetDir()
7280
if err != nil {
@@ -123,10 +131,18 @@ func (s *StepAddFloppy) Cleanup(state multistep.StateBag) {
123131
if UploadedFloppyPath, ok := state.GetOk("uploaded_floppy_path"); ok {
124132
ui.Say("Deleting floppy image...")
125133

126-
ds, err := d.FindDatastore(s.Datastore, s.Host)
127-
if err != nil {
128-
state.Put("error", err)
129-
return
134+
var ds driver.Datastore
135+
var err error
136+
137+
// If a datastore was resolved (from datastore or datastore_cluster), use it.
138+
if resolvedDs, ok := state.GetOk("datastore"); ok {
139+
ds = resolvedDs.(driver.Datastore)
140+
} else {
141+
ds, err = d.FindDatastore(s.Datastore, s.Host)
142+
if err != nil {
143+
state.Put("error", err)
144+
return
145+
}
130146
}
131147

132148
err = ds.Delete(UploadedFloppyPath.(string))

builder/vsphere/common/step_download.go

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ type StepDownload struct {
4646
}
4747

4848
func (s *StepDownload) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
49-
driver := state.Get("driver").(driver.Driver)
49+
d := state.Get("driver").(driver.Driver)
5050
ui := state.Get("ui").(packersdk.Ui)
5151

5252
// Set the remote cache datastore. If not set, use the default datastore for the build.
@@ -55,11 +55,19 @@ func (s *StepDownload) Run(ctx context.Context, state multistep.StateBag) multis
5555
remoteCacheDatastore = s.RemoteCacheDatastore
5656
}
5757

58-
// Find the datastore to use for the remote cache.
59-
ds, err := driver.FindDatastore(remoteCacheDatastore, s.Host)
60-
if err != nil {
61-
state.Put("error", fmt.Errorf("error finding the datastore: %v", err))
62-
return multistep.ActionHalt
58+
var ds driver.Datastore
59+
var err error
60+
61+
// If a datastore was resolved (from datastore or datastore_cluster), use it.
62+
if resolvedDs, ok := state.GetOk("datastore"); ok && remoteCacheDatastore == s.Datastore {
63+
ds = resolvedDs.(driver.Datastore)
64+
} else {
65+
// Find the datastore to use for the remote cache.
66+
ds, err = d.FindDatastore(remoteCacheDatastore, s.Host)
67+
if err != nil {
68+
state.Put("error", fmt.Errorf("error finding the datastore: %v", err))
69+
return multistep.ActionHalt
70+
}
6371
}
6472

6573
// Set the remote cache path. If not set, use the default cache path.

0 commit comments

Comments
 (0)