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
10 changes: 10 additions & 0 deletions builder/nutanix/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"fmt"
"log"
"net"
"strings"
"time"

"github.com/hashicorp/packer-plugin-sdk/bootcommand"
Expand Down Expand Up @@ -128,6 +129,7 @@ type VmConfig struct {
Core int64 `mapstructure:"core" json:"core" required:"false"`
MemoryMB int64 `mapstructure:"memory_mb" json:"memory_mb" required:"false"`
UserData string `mapstructure:"user_data" json:"user_data" required:"false"`
WindowsInstallType string `mapstructure:"windows_install_type" json:"windows_install_type" required:"false"`
VMCategories []Category `mapstructure:"vm_categories" required:"false"`
Project string `mapstructure:"project" required:"false"`
GPU []GPU `mapstructure:"gpu" required:"false"`
Expand Down Expand Up @@ -319,6 +321,14 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("missing nutanix_password"))
}

switch strings.ToUpper(c.VmConfig.WindowsInstallType) {
case "", "PREPARED", "FRESH":
// ok
default:
errs = packersdk.MultiErrorAppend(errs,
fmt.Errorf("windows_install_type must be FRESH or PREPARED, got %q", c.VmConfig.WindowsInstallType))
}

if c.VmConfig.VMName == "" {
p := fmt.Sprintf("Packer-%s", random.String(random.PossibleAlphaNumUpper, 8))
log.Println("No vmname assigned, setting to " + p)
Expand Down
4 changes: 4 additions & 0 deletions builder/nutanix/config.hcl2spec.go

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

53 changes: 53 additions & 0 deletions builder/nutanix/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package nutanix

import (
"strings"
"testing"
)

func minimalValidConfig(extra map[string]interface{}) map[string]interface{} {
cfg := map[string]interface{}{
"nutanix_endpoint": "pc.example.com",
"nutanix_username": "admin",
"nutanix_password": "password",
"cluster_name": "cluster-1",
"os_type": "Linux",
"communicator": "none",
"vm_disks": []map[string]interface{}{
{
"image_type": "DISK_IMAGE",
"source_image_name": "img",
"disk_size_gb": 40,
},
},
}
for k, v := range extra {
cfg[k] = v
}
return cfg
}

func TestPrepareRejectsInvalidWindowsInstallType(t *testing.T) {
c := &Config{}
_, err := c.Prepare(minimalValidConfig(map[string]interface{}{
"nutanix_username": "admin",
"nutanix_password": "password",
"windows_install_type": "fresh",
}))
if err != nil {
t.Fatalf("expected case-insensitive match to succeed, got: %v", err)
}

c2 := &Config{}
_, err = c2.Prepare(minimalValidConfig(map[string]interface{}{
"nutanix_username": "admin",
"nutanix_password": "password",
"windows_install_type": "INVALID",
}))
if err == nil {
t.Fatal("expected Prepare to fail with invalid windows_install_type")
}
if !strings.Contains(err.Error(), "windows_install_type must be FRESH or PREPARED") {
t.Errorf("unexpected error: %v", err)
}
}
6 changes: 6 additions & 0 deletions builder/nutanix/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -755,6 +755,12 @@ func (d *NutanixDriver) CreateRequest(ctx context.Context, vmConfig VmConfig, st
log.Printf("CloudInit configured for Linux VM")
} else if vmConfig.OSType == "Windows" {
sysprep := vmmModels.NewSysprep()
if strings.EqualFold(vmConfig.WindowsInstallType, "FRESH") {
sysprep.InstallType = vmmModels.INSTALLTYPE_FRESH.Ref()
} else {
sysprep.InstallType = vmmModels.INSTALLTYPE_PREPARED.Ref()
}
log.Printf("Windows Sysprep InstallType: %s", sysprep.InstallType.GetName())
unattendXml := vmmModels.NewUnattendxml()
unattendXml.Value = &vmConfig.UserData
sysprep.SysprepScript = vmmModels.NewOneOfSysprepSysprepScript()
Expand Down
20 changes: 12 additions & 8 deletions builder/nutanix/step_shutdown_vm.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,19 +53,23 @@ func (s *StepShutdown) Run(ctx context.Context, state multistep.StateBag) multis
log.Printf("executing shutdown command: %s", s.Command)
cmd := &packersdk.RemoteCmd{Command: s.Command}
if err := cmd.RunWithUi(ctx, comm, ui); err != nil {
err := fmt.Errorf("failed to send shutdown command: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
// Communicator errors during shutdown are expected — the
// shutdown command (e.g. sysprep /shutdown) may power off
// the VM before the communicator can read the exit status.
// Log a warning and fall through to the power-state polling
// loop, which will detect the VM is off.
log.Printf("shutdown command returned error (expected if VM is shutting down): %s", err)
ui.Say("Shutdown command disconnected (expected during sysprep/shutdown). Waiting for VM to power off...")
}

} else {
ui.Say("Halting the virtual machine...")
if err := driver.PowerOff(ctx, vmUUID); err != nil {
err := fmt.Errorf("error stopping VM: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
// PowerOff may fail if the VM is already off (e.g. sysprep
// /shutdown ran as the last provisioner). Log a warning and
// fall through to the polling loop.
log.Printf("PowerOff returned error (may be expected if VM already shut down): %s", err)
ui.Say("PowerOff returned error — checking if VM is already off...")
}
}
} else {
Expand Down
Loading