Skip to content

Commit 730bb46

Browse files
committed
feat: clone from .ovf/.ova
Add support for cloning virtual machines from .ovf and .ova files using ovftool, in addition to .vmx files. Signed-off-by: Ryan Johnson <ryan.johnson@broadcom.com>
1 parent d80f546 commit 730bb46

7 files changed

Lines changed: 172 additions & 41 deletions

File tree

.web-docs/components/builder/vmx/README.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ JSON Example:
5151

5252
<!-- Code generated from the comments of the Config struct in builder/vmware/vmx/config.go; DO NOT EDIT MANUALLY -->
5353

54-
- `source_path` (string) - Path to the source `.vmx` file to clone.
54+
- `source_path` (string) - Path to the source `.vmx`, '.ovf', or '.ova' file to clone.
5555

5656
<!-- End of code generated from the comments of the Config struct in builder/vmware/vmx/config.go; -->
5757

@@ -90,6 +90,18 @@ JSON Example:
9090
- `snapshot_name` (string) - This is the name of the initial snapshot created after provisioning and
9191
cleanup. If blank, no snapshot is created.
9292

93+
- `guest_os_type` (string) - The guest operating system identifier for the virtual machine.
94+
95+
~> **Note:** This is required when cloning from an OVF/OVA file
96+
and overrides the guest operating system identifier set by ovftool.
97+
98+
- `version` (int) - The virtual machine hardware version. Refer to [KB 315655](https://knowledge.broadcom.com/external/article?articleNumber=315655)
99+
for more information on supported virtual hardware versions.
100+
Default is 21. Minimum is 19.
101+
102+
~> **Note:** This is only used when cloning from an OVF/OVA file
103+
and overrides the hardware version set by ovftool.
104+
93105
<!-- End of code generated from the comments of the Config struct in builder/vmware/vmx/config.go; -->
94106

95107

builder/vmware/vmx/builder.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -100,11 +100,13 @@ func (b *Builder) Run(ctx context.Context, ui packersdk.Ui, hook packersdk.Hook)
100100
DiskTypeId: b.config.DiskTypeId,
101101
},
102102
&StepCloneVMX{
103-
Path: b.config.SourcePath,
104-
OutputDir: &b.config.OutputDir,
105-
VMName: b.config.VMName,
106-
Linked: b.config.Linked,
107-
Snapshot: b.config.AttachSnapshot,
103+
Path: b.config.SourcePath,
104+
OutputDir: &b.config.OutputDir,
105+
VMName: b.config.VMName,
106+
Linked: b.config.Linked,
107+
Snapshot: b.config.AttachSnapshot,
108+
Version: b.config.Version,
109+
GuestOSType: b.config.GuestOSType,
108110
},
109111
&vmwcommon.StepConfigureVMX{
110112
CustomData: b.config.VMXData,

builder/vmware/vmx/config.go

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ type Config struct {
6161
// virtual machine is started from its current state. Default to
6262
// `null/empty`.
6363
AttachSnapshot string `mapstructure:"attach_snapshot" required:"false"`
64-
// Path to the source `.vmx` file to clone.
64+
// Path to the source `.vmx`, '.ovf', or '.ova' file to clone.
6565
SourcePath string `mapstructure:"source_path" required:"true"`
6666
// This is the name of the `.vmx` file for the virtual machine, without
6767
// the file extension. By default, this is `packer-BUILDNAME`, where
@@ -70,6 +70,18 @@ type Config struct {
7070
// This is the name of the initial snapshot created after provisioning and
7171
// cleanup. If blank, no snapshot is created.
7272
SnapshotName string `mapstructure:"snapshot_name" required:"false"`
73+
// The guest operating system identifier for the virtual machine.
74+
//
75+
// ~> **Note:** This is required when cloning from an OVF/OVA file
76+
// and overrides the guest operating system identifier set by ovftool.
77+
GuestOSType string `mapstructure:"guest_os_type" required:"false"`
78+
// The virtual machine hardware version. Refer to [KB 315655](https://knowledge.broadcom.com/external/article?articleNumber=315655)
79+
// for more information on supported virtual hardware versions.
80+
// Default is 21. Minimum is 19.
81+
//
82+
// ~> **Note:** This is only used when cloning from an OVF/OVA file
83+
// and overrides the hardware version set by ovftool.
84+
Version int `mapstructure:"version" required:"false"`
7385

7486
ctx interpolate.Context
7587
}
@@ -135,6 +147,19 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
135147
errs = packersdk.MultiErrorAppend(errs,
136148
fmt.Errorf("source_path is invalid: %s", err))
137149
}
150+
151+
// Check if source is OVA/OVF and validate requirements.
152+
lowerPath := strings.ToLower(c.SourcePath)
153+
if strings.HasSuffix(lowerPath, ".ova") || strings.HasSuffix(lowerPath, ".ovf") {
154+
if vmwcommon.GetOvfTool() == "" {
155+
errs = packersdk.MultiErrorAppend(errs,
156+
errors.New("ovftool is required to clone from OVA/OVF files but was not found in PATH"))
157+
}
158+
if c.GuestOSType == "" {
159+
errs = packersdk.MultiErrorAppend(errs,
160+
errors.New("'guest_os_type' is required when cloning from OVA/OVF files"))
161+
}
162+
}
138163
}
139164

140165
if c.Headless && c.DisableVNC {
@@ -155,6 +180,13 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
155180
c.SkipExport = true
156181
}
157182

183+
// Set default hardware version for OVF/OVA sources, if not specified.
184+
if c.Version == 0 {
185+
c.Version = vmwcommon.DefaultHardwareVersion
186+
} else if c.Version < vmwcommon.MinimumHardwareVersion {
187+
errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("invalid 'version' %d, minimum hardware version: %d", c.Version, vmwcommon.MinimumHardwareVersion))
188+
}
189+
158190
err = c.Validate(c.SkipExport)
159191
if err != nil {
160192
errs = packersdk.MultiErrorAppend(errs, err)

builder/vmware/vmx/config.hcl2spec.go

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

builder/vmware/vmx/step_clone_vmx.go

Lines changed: 102 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -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.
2122
type 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)

docs-partials/builder/vmware/vmx/Config-not-required.mdx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,16 @@
3030
- `snapshot_name` (string) - This is the name of the initial snapshot created after provisioning and
3131
cleanup. If blank, no snapshot is created.
3232

33+
- `guest_os_type` (string) - The guest operating system identifier for the virtual machine.
34+
35+
~> **Note:** This is required when cloning from an OVF/OVA file
36+
and overrides the guest operating system identifier set by ovftool.
37+
38+
- `version` (int) - The virtual machine hardware version. Refer to [KB 315655](https://knowledge.broadcom.com/external/article?articleNumber=315655)
39+
for more information on supported virtual hardware versions.
40+
Default is 21. Minimum is 19.
41+
42+
~> **Note:** This is only used when cloning from an OVF/OVA file
43+
and overrides the hardware version set by ovftool.
44+
3345
<!-- End of code generated from the comments of the Config struct in builder/vmware/vmx/config.go; -->
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<!-- Code generated from the comments of the Config struct in builder/vmware/vmx/config.go; DO NOT EDIT MANUALLY -->
22

3-
- `source_path` (string) - Path to the source `.vmx` file to clone.
3+
- `source_path` (string) - Path to the source `.vmx`, '.ovf', or '.ova' file to clone.
44

55
<!-- End of code generated from the comments of the Config struct in builder/vmware/vmx/config.go; -->

0 commit comments

Comments
 (0)