@@ -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,95 @@ 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 .ova/.ovf file, use ovftool.
47+ if strings .HasSuffix (lowerSrc , ".ovf" ) || strings .HasSuffix (lowerSrc , ".ova" ) {
48+ // Clone the source virtual machine from the .ova/.ovf file.
49+ ui .Sayf ("Cloning from source .ova/.ovf..." )
50+ log .Printf ("[INFO] Cloning from: %s" , s .Path )
51+ log .Printf ("[INFO] Cloning to: %s" , * s .OutputDir )
52+
53+ ovftool := vmwcommon .GetOvfTool ()
54+ if ovftool == "" {
55+ return halt (fmt .Errorf ("ovftool not found in PATH" ))
56+ }
57+
58+ // Ensure that the output directory exists.
59+ if err := os .MkdirAll (* s .OutputDir , 0o755 ); err != nil {
60+ return halt (fmt .Errorf ("failed to create output directory: %w" , err ))
61+ }
62+
63+ args := []string {
64+ "--lax" ,
65+ fmt .Sprintf ("--maxVirtualHardwareVersion=%d" , s .Version ),
66+ fmt .Sprintf ("--name=%s" , s .VMName ),
67+ s .Path ,
68+ * s .OutputDir ,
69+ }
70+
71+ cmd := exec .CommandContext (ctx , ovftool , args ... )
72+ cmd .Stdout = os .Stdout
73+ cmd .Stderr = os .Stderr
74+
75+ if err := cmd .Run (); err != nil {
76+ return halt (fmt .Errorf ("failed to clone from .ovf/.ova: %w" , err ))
77+ }
78+
79+ ui .Say ("Successfully cloned from .ovf/.ova." )
80+
81+ // Find the .vmx file created by ovftool.
82+ vmxPath = filepath .Join (* s .OutputDir , s .VMName + ".vmx" )
83+ if _ , err := os .Stat (vmxPath ); os .IsNotExist (err ) {
84+ // Check for .vmwarevm bundle path in output directory.
85+ vmxPath = filepath .Join (* s .OutputDir , s .VMName + ".vmwarevm" , s .VMName + ".vmx" )
86+ if _ , err := os .Stat (vmxPath ); os .IsNotExist (err ) {
87+ // Search for any .vmx file in the output directory.
88+ var found bool
89+ err := filepath .Walk (* s .OutputDir , func (path string , info os.FileInfo , err error ) error {
90+ if err != nil {
91+ return err
92+ }
93+ if ! info .IsDir () && strings .HasSuffix (strings .ToLower (path ), ".vmx" ) {
94+ vmxPath = path
95+ found = true
96+ return filepath .SkipAll
97+ }
98+ return nil
99+ })
100+ if err != nil || ! found {
101+ return halt (fmt .Errorf ("unable to find .vmx file after ovftool conversion" ))
102+ }
103+ }
104+ }
105+
106+ // Override guest operating system identifier, if specified.
107+ if s .GuestOSType != "" {
108+ log .Printf ("[INFO] Overriding operating system identifier,: %s" , s .GuestOSType )
109+ vmxData , err := vmwcommon .ReadVMX (vmxPath )
110+ if err != nil {
111+ return halt (fmt .Errorf ("failed to read vmx: %w" , err ))
112+ }
113+
114+ vmxData ["guestos" ] = s .GuestOSType
115+
116+ if err := vmwcommon .WriteVMX (vmxPath , vmxData ); err != nil {
117+ return halt (fmt .Errorf ("failed to write vmx: %w" , err ))
118+ }
119+ }
120+ } else {
121+ // Clone the source virtual machine from the .vmx configuration file.
122+ ui .Say ("Cloning from source .vmx..." )
123+ vmxPath = filepath .Join (* s .OutputDir , s .VMName + ".vmx" )
124+ log .Printf ("[INFO] Cloning from: %s" , s .Path )
125+ log .Printf ("[INFO] Cloning to: %s" , vmxPath )
126+
127+ if err := driver .Clone (vmxPath , s .Path , s .Linked , s .Snapshot ); err != nil {
128+ return halt (fmt .Errorf ("failed to clone from .vmx: %s" , err ))
129+ }
130+
131+ ui .Say ("Successfully cloned from .vmx." )
48132 }
49133
50134 // Read in the virtual machine configuration from the cloned .vmx file.
@@ -54,20 +138,6 @@ func (s *StepCloneVMX) Run(ctx context.Context, state multistep.StateBag) multis
54138 }
55139
56140 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
71141 diskPathKeyRe := regexp .MustCompile (`(?i)^(scsi|sata|ide|nvme)[[:digit:]]:[[:digit:]]{1,2}\.fileName` )
72142 for k , v := range vmxData {
73143 match := diskPathKeyRe .FindString (k )
@@ -76,18 +146,18 @@ func (s *StepCloneVMX) Run(ctx context.Context, state multistep.StateBag) multis
76146 }
77147 }
78148
79- // Build the full path to each disk.
80149 var diskFullPaths []string
150+ vmxDir := filepath .Dir (vmxPath )
81151 for _ , diskFilename := range diskFilenames {
82152 log .Printf ("[INFO] Found attached disk with filename: %s" , diskFilename )
83- diskFullPaths = append (diskFullPaths , filepath .Join (* s .OutputDir , diskFilename ))
153+ // Disk paths are relative to the .vmx file location, not OutputDir.
154+ diskFullPaths = append (diskFullPaths , filepath .Join (vmxDir , diskFilename ))
84155 }
85156
86157 if len (diskFullPaths ) == 0 {
87158 return halt (fmt .Errorf ("unable to enumerate disk info from the vmx file" ))
88159 }
89160
90- // Determine the network type by reading out of the .vmx.
91161 var networkType string
92162 if _ , ok := vmxData ["ethernet0.connectiontype" ]; ok {
93163 networkType = vmxData ["ethernet0.connectiontype" ]
@@ -98,7 +168,6 @@ func (s *StepCloneVMX) Run(ctx context.Context, state multistep.StateBag) multis
98168 log .Printf ("[INFO] Defaulting to network type: %s" , networkType )
99169 }
100170
101- // Stash all required information in state.
102171 state .Put ("vmx_path" , vmxPath )
103172 state .Put ("disk_full_paths" , diskFullPaths )
104173 state .Put ("vmnetwork" , networkType )
0 commit comments