@@ -8,23 +8,26 @@ import (
88 "fmt"
99 "log"
1010 "os"
11+ "os/exec"
1112 "path/filepath"
1213 "regexp"
14+ "strings"
1315
1416 "github.com/hashicorp/packer-plugin-sdk/multistep"
1517 packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
1618 vmwcommon "github.com/hashicorp/packer-plugin-vmware/builder/vmware/common"
1719)
1820
19- // StepCloneVMX takes a .vmx file and clones the virtual machine into the
20- // output directory.
21+ // StepCloneVMX clones the source virtual machine a supplied path.
2122type StepCloneVMX struct {
22- OutputDir * string
23- Path string
24- VMName string
25- Linked bool
26- Snapshot string
27- tempDir string
23+ OutputDir * string
24+ Path string
25+ VMName string
26+ Linked bool
27+ Snapshot string
28+ Version int
29+ GuestOSType string
30+ tempDir string
2831}
2932
3033// Run executes the VMX cloning step, creating a copy of the source virtual machine.
@@ -37,14 +40,139 @@ func (s *StepCloneVMX) Run(ctx context.Context, state multistep.StateBag) multis
3740 driver := state .Get ("driver" ).(vmwcommon.Driver )
3841 ui := state .Get ("ui" ).(packersdk.Ui )
3942
40- // Set the path we want for the new .vmx file and clone.
41- vmxPath := filepath .Join (* s .OutputDir , s .VMName + ".vmx" )
42- ui .Say ("Cloning source virtual machine..." )
43- log .Printf ("[INFO] Cloning from: %s" , s .Path )
44- log .Printf ("[INFO] Cloning to: %s" , vmxPath )
43+ lowerSrc := strings .ToLower (s .Path )
44+ var vmxPath string
4545
46- if err := driver .Clone (vmxPath , s .Path , s .Linked , s .Snapshot ); err != nil {
47- return halt (err )
46+ // If the source is a .ovf/.ova file, use ovftool.
47+ if strings .HasSuffix (lowerSrc , ".ovf" ) || strings .HasSuffix (lowerSrc , ".ova" ) {
48+ // Clone the source virtual machine from the .ovf/.ova file.
49+ ui .Sayf ("Cloning from source .ovf/.ova..." )
50+ log .Printf ("[INFO] Cloning from: %s" , s .Path )
51+ log .Printf ("[INFO] Cloning to: %s" , * s .OutputDir )
52+
53+ // ovftool always creates a subdirectory with the virtual machine name.
54+ // Pass the output directory to ovftool, then move the contents up one level.
55+ ovftoolTargetDir := * s .OutputDir
56+
57+ // Ensure that the output directory exists.
58+ if err := os .MkdirAll (ovftoolTargetDir , 0o755 ); err != nil {
59+ return halt (fmt .Errorf ("failed to create output directory: %w" , err ))
60+ }
61+
62+ // Set up the ovftool command.
63+ ovftool := vmwcommon .GetOvfTool ()
64+
65+ // Pass the virtual machine name, virtual hardware version, and output directory to ovftool.
66+ args := []string {
67+ "--lax" ,
68+ fmt .Sprintf ("--maxVirtualHardwareVersion=%d" , s .Version ),
69+ fmt .Sprintf ("--name=%s" , s .VMName ),
70+ s .Path ,
71+ ovftoolTargetDir ,
72+ }
73+
74+ cmd := exec .CommandContext (ctx , ovftool , args ... )
75+ cmd .Stdout = os .Stdout
76+ cmd .Stderr = os .Stderr
77+
78+ if err := cmd .Run (); err != nil {
79+ return halt (fmt .Errorf ("failed to clone from .ovf/.ova: %w" , err ))
80+ }
81+
82+ ui .Say ("Successfully cloned from .ovf/.ova." )
83+
84+ // Determine where ovftool actually created the output within the target directory.
85+ // ovftool creates either <target>/<vmname> or <target>/<vmname>.vmwarevm depending on the platform.
86+ ovftoolCreatedPath := filepath .Join (ovftoolTargetDir , s .VMName )
87+ if _ , err := os .Stat (ovftoolCreatedPath ); os .IsNotExist (err ) {
88+ // Check if ovftool created a .vmwarevm bundle instead (VMware Fusion on macOS).
89+ vmwarevmPath := ovftoolCreatedPath + ".vmwarevm"
90+ if _ , err := os .Stat (vmwarevmPath ); err == nil {
91+ ovftoolCreatedPath = vmwarevmPath
92+ } else {
93+ return halt (fmt .Errorf ("ovftool output not found at %s or %s" , ovftoolCreatedPath , vmwarevmPath ))
94+ }
95+ }
96+
97+ // Move the ovftool output contents to the root of the output directory.
98+ // Use a temporary directory outside the output directory to avoid conflicts.
99+ log .Printf ("[INFO] Moving output from %s to %s" , ovftoolCreatedPath , * s .OutputDir )
100+ tempDir := strings .TrimRight (* s .OutputDir , string (filepath .Separator )) + ".tmp"
101+ s .tempDir = tempDir
102+ if err := os .Rename (ovftoolCreatedPath , tempDir ); err != nil {
103+ return halt (fmt .Errorf ("failed to rename ovftool output: %w" , err ))
104+ }
105+
106+ // Remove the output directory.
107+ if err := os .RemoveAll (* s .OutputDir ); err != nil && ! os .IsNotExist (err ) {
108+ os .Rename (tempDir , ovftoolCreatedPath )
109+ return halt (fmt .Errorf ("failed to remove output directory: %w" , err ))
110+ }
111+
112+ // Ensure parent directories exist before the final move.
113+ // Use the cleaned output directory path to get the correct parent.
114+ cleanedOutputDir := strings .TrimRight (* s .OutputDir , string (filepath .Separator ))
115+ if err := os .MkdirAll (filepath .Dir (cleanedOutputDir ), 0o755 ); err != nil {
116+ return halt (fmt .Errorf ("failed to create parent directories: %w" , err ))
117+ }
118+
119+ // Move the temporary directory to the final output location.
120+ if err := os .Rename (tempDir , * s .OutputDir ); err != nil {
121+ return halt (fmt .Errorf ("failed to move ovftool results to output directory: %w" , err ))
122+ }
123+ s .tempDir = ""
124+
125+ // Find the .vmx file in the output directory.
126+ vmxPath = filepath .Join (* s .OutputDir , s .VMName + ".vmx" )
127+ if _ , err := os .Stat (vmxPath ); os .IsNotExist (err ) {
128+ // VMware Fusion: Check for .vmwarevm bundle from ovftool.
129+ vmxPath = filepath .Join (* s .OutputDir , s .VMName + ".vmwarevm" , s .VMName + ".vmx" )
130+ if _ , err := os .Stat (vmxPath ); os .IsNotExist (err ) {
131+ // Search for any .vmx file in the output directory.
132+ var found bool
133+ err := filepath .Walk (* s .OutputDir , func (path string , info os.FileInfo , err error ) error {
134+ if err != nil {
135+ return err
136+ }
137+ if ! info .IsDir () && strings .HasSuffix (strings .ToLower (path ), ".vmx" ) {
138+ vmxPath = path
139+ found = true
140+ return filepath .SkipAll
141+ }
142+ return nil
143+ })
144+ if err != nil || ! found {
145+ return halt (fmt .Errorf ("unable to find .vmx file after ovftool conversion" ))
146+ }
147+ }
148+ }
149+
150+ // Override guest operating system identifier, if specified.
151+ if s .GuestOSType != "" {
152+ log .Printf ("[INFO] Overriding guest operating system identifier set by ovftool: %s" , s .GuestOSType )
153+ vmxData , err := vmwcommon .ReadVMX (vmxPath )
154+ if err != nil {
155+ return halt (fmt .Errorf ("failed to read vmx: %w" , err ))
156+ }
157+
158+ vmxData ["guestos" ] = s .GuestOSType
159+
160+ if err := vmwcommon .WriteVMX (vmxPath , vmxData ); err != nil {
161+ return halt (fmt .Errorf ("failed to write vmx: %w" , err ))
162+ }
163+ }
164+ } else {
165+ // Clone the source virtual machine from the .vmx configuration file.
166+ ui .Say ("Cloning from source .vmx..." )
167+ vmxPath = filepath .Join (* s .OutputDir , s .VMName + ".vmx" )
168+ log .Printf ("[INFO] Cloning from: %s" , s .Path )
169+ log .Printf ("[INFO] Cloning to: %s" , vmxPath )
170+
171+ if err := driver .Clone (vmxPath , s .Path , s .Linked , s .Snapshot ); err != nil {
172+ return halt (fmt .Errorf ("failed to clone from .vmx: %s" , err ))
173+ }
174+
175+ ui .Say ("Successfully cloned from .vmx." )
48176 }
49177
50178 // Read in the virtual machine configuration from the cloned .vmx file.
@@ -54,20 +182,6 @@ func (s *StepCloneVMX) Run(ctx context.Context, state multistep.StateBag) multis
54182 }
55183
56184 var diskFilenames []string
57- // The VMX file stores the path to a configured disk, and information
58- // about that disks attachment to a virtual adapter/controller, as a
59- // key/value pair.
60- //
61- // For a virtual disk attached to bus ID 3 of the virtual machines
62- // first SCSI adapter the key/value pair would look something like:
63- // scsi0:3.fileName = "relative/path/to/scsiDisk.vmdk"
64- // The supported adapter types and configuration maximums for each type
65- // vary according to the hypervisor and version, and the virtua
66- // machine hardware version used.
67- //
68- // The following regexp is used to match all possible disk attachment
69- // points that may be found in the VMX file across all VMware
70- // platforms/versions and Virtual Machine Hardware versions
71185 diskPathKeyRe := regexp .MustCompile (`(?i)^(scsi|sata|ide|nvme)[[:digit:]]:[[:digit:]]{1,2}\.fileName` )
72186 for k , v := range vmxData {
73187 match := diskPathKeyRe .FindString (k )
@@ -76,18 +190,18 @@ func (s *StepCloneVMX) Run(ctx context.Context, state multistep.StateBag) multis
76190 }
77191 }
78192
79- // Build the full path to each disk.
80193 var diskFullPaths []string
194+ vmxDir := filepath .Dir (vmxPath )
81195 for _ , diskFilename := range diskFilenames {
82196 log .Printf ("[INFO] Found attached disk with filename: %s" , diskFilename )
83- diskFullPaths = append (diskFullPaths , filepath .Join (* s .OutputDir , diskFilename ))
197+ // Disk paths are relative to the .vmx file location, not OutputDir.
198+ diskFullPaths = append (diskFullPaths , filepath .Join (vmxDir , diskFilename ))
84199 }
85200
86201 if len (diskFullPaths ) == 0 {
87202 return halt (fmt .Errorf ("unable to enumerate disk info from the vmx file" ))
88203 }
89204
90- // Determine the network type by reading out of the .vmx.
91205 var networkType string
92206 if _ , ok := vmxData ["ethernet0.connectiontype" ]; ok {
93207 networkType = vmxData ["ethernet0.connectiontype" ]
@@ -98,7 +212,6 @@ func (s *StepCloneVMX) Run(ctx context.Context, state multistep.StateBag) multis
98212 log .Printf ("[INFO] Defaulting to network type: %s" , networkType )
99213 }
100214
101- // Stash all required information in state.
102215 state .Put ("vmx_path" , vmxPath )
103216 state .Put ("disk_full_paths" , diskFullPaths )
104217 state .Put ("vmnetwork" , networkType )
0 commit comments