Skip to content

Commit 52d6365

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 d198c80 commit 52d6365

28 files changed

+1091
-77
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

GNUmakefile

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ COUNT?=1
66
TEST?=$(shell go list ./...)
77
HASHICORP_PACKER_PLUGIN_SDK_VERSION?=$(shell go list -m github.com/hashicorp/packer-plugin-sdk | cut -d " " -f2)
88

9+
# Use Go 1.23.12 specifically for packer-sdc related commands
10+
GO_SDC ?= env GOTOOLCHAIN=go1.23.12 go
11+
912
.PHONY: dev
1013

1114
build:
@@ -19,7 +22,7 @@ test:
1922
@go test -race -count $(COUNT) $(TEST) -timeout=3m
2023

2124
install-packer-sdc: ## Install packer sofware development command
22-
@go install github.com/hashicorp/packer-plugin-sdk/cmd/packer-sdc@${HASHICORP_PACKER_PLUGIN_SDK_VERSION}
25+
@$(GO_SDC) install github.com/hashicorp/packer-plugin-sdk/cmd/packer-sdc@${HASHICORP_PACKER_PLUGIN_SDK_VERSION}
2326

2427
plugin-check: install-packer-sdc build
2528
@packer-sdc plugin-check ${BINARY}
@@ -28,7 +31,7 @@ testacc: dev
2831
@PACKER_ACC=1 go test -count $(COUNT) -v $(TEST) -timeout=120m
2932

3033
generate: install-packer-sdc
31-
@go generate ./...
34+
@$(GO_SDC) generate ./...
3235
@rm -rf .docs
3336
@packer-sdc renderdocs -src "docs" -partials docs-partials/ -dst ".docs/"
3437
@./.web-docs/scripts/compile-to-webdocs.sh "." ".docs" ".web-docs" "hashicorp"

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))

0 commit comments

Comments
 (0)