Skip to content

Commit e77dccf

Browse files
authored
Obtain default values for certain VM configs from Virtualbox (#152)
* feat(virtualbox): add support for retrieving and applying VM defaults - Introduced a new `stepGetVMDefaults` step to retrieve default VM settings using `VBoxManage`. - Updated `stepCreateVM` to use the retrieved defaults for the graphics controller if not explicitly configured. - Added a new `VBoxManageWithOutput` method to the `Driver` interface for commands that return output. - Refactored `Prepare` method to validate `GfxController` only when explicitly set. This change improves flexibility by allowing default VirtualBox settings to be dynamically applied. * improved acc test * update docs
1 parent dc36fe5 commit e77dccf

13 files changed

Lines changed: 435 additions & 5 deletions

File tree

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ necessary for this build to succeed and can be found further down the page.
109109
When set to vboxsvga, the graphics controller is VirtualBox SVGA.
110110
When set to vmsvga, the graphics controller is VMware SVGA.
111111
When set to none, the graphics controller is disabled.
112+
When this configuration is omitted, the default value is determined by VirtualBox.
112113

113114
- `gfx_vram_size` (uint) - The VRAM size to be used. By default, this is 4 MiB.
114115

builder/virtualbox/common/driver.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,13 @@ type Driver interface {
5757
SuppressMessages() error
5858

5959
// VBoxManage executes the given VBoxManage command
60-
// and returns the stdout channel as string
60+
// and returns an error
6161
VBoxManage(...string) error
6262

63+
// VBoxManage executes the given VBoxManage command
64+
// and returns the stdout channel as string
65+
VBoxManageWithOutput(args ...string) (string, error)
66+
6367
// Verify checks to make sure that this driver should function
6468
// properly. If there is any indication the driver can't function,
6569
// this will return an error.

builder/virtualbox/common/driver_mock.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ type DriverMock struct {
5454
VBoxManageCalls [][]string
5555
VBoxManageErrs []error
5656

57+
VBoxManageWithOutputCalls [][]string
58+
VBoxManageWithOutputErrs []error
59+
5760
VerifyCalled bool
5861
VerifyErr error
5962

@@ -153,6 +156,15 @@ func (d *DriverMock) VBoxManage(args ...string) error {
153156
return nil
154157
}
155158

159+
func (d *DriverMock) VBoxManageWithOutput(args ...string) (string, error) {
160+
d.VBoxManageCalls = append(d.VBoxManageCalls, args)
161+
162+
if len(d.VBoxManageErrs) >= len(d.VBoxManageCalls) {
163+
return "", d.VBoxManageErrs[len(d.VBoxManageCalls)-1]
164+
}
165+
return "", nil
166+
}
167+
156168
func (d *DriverMock) Verify() error {
157169
d.VerifyCalled = true
158170
return d.VerifyErr

builder/virtualbox/iso/builder.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ type Config struct {
8686
// When set to vboxsvga, the graphics controller is VirtualBox SVGA.
8787
// When set to vmsvga, the graphics controller is VMware SVGA.
8888
// When set to none, the graphics controller is disabled.
89+
// When this configuration is omitted, the default value is determined by VirtualBox.
8990
GfxController string `mapstructure:"gfx_controller" required:"false"`
9091
// The VRAM size to be used. By default, this is 4 MiB.
9192
GfxVramSize uint `mapstructure:"gfx_vram_size" required:"false"`
@@ -271,11 +272,8 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, []string, error) {
271272
errs, errors.New("NIC type can only be 82540EM, 82543GC, 82545EM, Am79C970A, Am79C973, Am79C960 or virtio"))
272273
}
273274

274-
if b.config.GfxController == "" {
275-
b.config.GfxController = "vboxvga"
276-
}
277275
switch b.config.GfxController {
278-
case "vboxvga", "vboxsvga", "vmsvga", "none":
276+
case "vboxvga", "vboxsvga", "vmsvga", "none", "":
279277
// do nothing
280278
default:
281279
errs = packersdk.MultiErrorAppend(
@@ -381,6 +379,7 @@ func (b *Builder) Run(ctx context.Context, ui packersdk.Ui, hook packersdk.Hook)
381379
}
382380

383381
steps := []multistep.Step{
382+
new(stepGetVMDefaults),
384383
&vboxcommon.StepDownloadGuestAdditions{
385384
GuestAdditionsMode: b.config.GuestAdditionsMode,
386385
GuestAdditionsURL: b.config.GuestAdditionsURL,

builder/virtualbox/iso/builder_acc_test.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@ package iso
66
import (
77
"fmt"
88
"io/ioutil"
9+
"os"
910
"os/exec"
1011
"path/filepath"
12+
"strings"
1113
"testing"
1214

1315
"github.com/hashicorp/packer-plugin-sdk/acctest"
@@ -39,3 +41,85 @@ func TestAccBuilder_basic(t *testing.T) {
3941
}
4042
acctest.TestPlugin(t, testCase)
4143
}
44+
45+
func TestAccBuilder_defaultGfxController(t *testing.T) {
46+
templatePath := filepath.Join("testdata", "default_gfx.pkr.hcl")
47+
bytes, err := ioutil.ReadFile(templatePath)
48+
if err != nil {
49+
t.Fatalf("failed to load template file %s", templatePath)
50+
}
51+
52+
testCase := &acctest.PluginTestCase{
53+
Name: "virtualbox-iso_default_gfx_test",
54+
Template: string(bytes),
55+
Teardown: func() error {
56+
testutils.CleanupFiles("output-ubuntu_2404")
57+
return nil
58+
},
59+
Check: func(buildCommand *exec.Cmd, logfile string) error {
60+
if buildCommand.ProcessState != nil {
61+
if buildCommand.ProcessState.ExitCode() != 0 {
62+
return fmt.Errorf("Bad exit code. Logfile: %s", logfile)
63+
}
64+
}
65+
return nil
66+
},
67+
}
68+
acctest.TestPlugin(t, testCase)
69+
}
70+
71+
func TestAccBuilder_incorrectGfxController(t *testing.T) {
72+
templatePath := filepath.Join("testdata", "incorrect_gfx.pkr.hcl")
73+
bytes, err := ioutil.ReadFile(templatePath)
74+
if err != nil {
75+
t.Fatalf("failed to load template file %s", templatePath)
76+
}
77+
78+
testCase := &acctest.PluginTestCase{
79+
Name: "virtualbox-iso_incorrect_gfx_test",
80+
Template: string(bytes),
81+
Teardown: func() error {
82+
testutils.CleanupFiles("output-ubuntu_2404")
83+
return nil
84+
},
85+
Check: func(buildCommand *exec.Cmd, logfile string) error {
86+
content, err := os.ReadFile(logfile)
87+
if err != nil {
88+
return fmt.Errorf("failed to read logfile: %v", err)
89+
}
90+
if buildCommand.ProcessState != nil {
91+
if buildCommand.ProcessState.ExitCode() == 0 || !strings.Contains(string(content), "Failed to construct device 'vga'") {
92+
return fmt.Errorf("Bad exit code. Logfile: %s", logfile)
93+
}
94+
}
95+
return nil
96+
},
97+
}
98+
acctest.TestPlugin(t, testCase)
99+
}
100+
101+
func TestAccBuilder_headlessMode(t *testing.T) {
102+
templatePath := filepath.Join("testdata", "headless.pkr.hcl")
103+
bytes, err := ioutil.ReadFile(templatePath)
104+
if err != nil {
105+
t.Fatalf("failed to load template file %s", templatePath)
106+
}
107+
108+
testCase := &acctest.PluginTestCase{
109+
Name: "virtualbox-iso_headless_test",
110+
Template: string(bytes),
111+
Teardown: func() error {
112+
testutils.CleanupFiles("output-ubuntu_2404")
113+
return nil
114+
},
115+
Check: func(buildCommand *exec.Cmd, logfile string) error {
116+
if buildCommand.ProcessState != nil {
117+
if buildCommand.ProcessState.ExitCode() != 0 {
118+
return fmt.Errorf("Bad exit code. Logfile: %s", logfile)
119+
}
120+
}
121+
return nil
122+
},
123+
}
124+
acctest.TestPlugin(t, testCase)
125+
}

builder/virtualbox/iso/step_create_vm.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,19 @@ func (s *stepCreateVM) Run(ctx context.Context, state multistep.StateBag) multis
7070
"--nictype6", config.NICType,
7171
"--nictype7", config.NICType,
7272
"--nictype8", config.NICType})
73+
74+
// Set the graphics controller, defaulting to "vboxvga" unless overridden by "vmDefaults" or config.
75+
if config.GfxController == "" {
76+
config.GfxController = "vboxvga"
77+
vmDefaultConfigs, defaultConfigsOk := state.GetOk("vmDefaults")
78+
if defaultConfigsOk {
79+
vmDefaultConfigs := vmDefaultConfigs.(map[string]string)
80+
if _, ok := vmDefaultConfigs["graphicscontroller"]; ok {
81+
config.GfxController = vmDefaultConfigs["graphicscontroller"]
82+
}
83+
}
84+
}
85+
7386
commands = append(commands, []string{"modifyvm", name, "--graphicscontroller", config.GfxController, "--vram", strconv.FormatUint(uint64(config.GfxVramSize), 10)})
7487
if config.RTCTimeBase == "UTC" {
7588
commands = append(commands, []string{"modifyvm", name, "--rtcuseutc", "on"})
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package iso
5+
6+
import (
7+
"context"
8+
"fmt"
9+
"log"
10+
"os"
11+
"path/filepath"
12+
"strings"
13+
"time"
14+
15+
"github.com/hashicorp/packer-plugin-sdk/multistep"
16+
vboxcommon "github.com/hashicorp/packer-plugin-virtualbox/builder/virtualbox/common"
17+
)
18+
19+
type stepGetVMDefaults struct {
20+
}
21+
22+
func (s *stepGetVMDefaults) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
23+
config := state.Get("config").(*Config)
24+
driver := state.Get("driver").(vboxcommon.Driver)
25+
26+
//Add a default value for vmDefaults in the state bag
27+
state.Put("vmDefaults", make(map[string]string))
28+
29+
baseFolder := os.TempDir()
30+
vmName := fmt.Sprintf("packer_temp_vm_%d", time.Now().Unix())
31+
defer os.RemoveAll(filepath.Join(baseFolder, vmName))
32+
33+
// Create temp VM
34+
command := []string{"createvm", "--name", vmName, "--ostype", config.GuestOSType, "--register", "--default", "--basefolder", baseFolder}
35+
if err := driver.VBoxManage(command...); err != nil {
36+
err := fmt.Errorf("Failed to obtain VM defaults: %s", err)
37+
log.Println(err)
38+
return multistep.ActionContinue
39+
}
40+
41+
defer func() {
42+
// Delete the temp VM
43+
command = []string{"unregistervm", vmName, "--delete"}
44+
if err := driver.VBoxManage(command...); err != nil {
45+
err := fmt.Errorf("Error deleting temp VM: %s", err)
46+
log.Println(err)
47+
}
48+
49+
}()
50+
51+
// Get the temp VM defaults
52+
command = []string{"showvminfo", vmName, "--machinereadable"}
53+
vminfo, err := driver.VBoxManageWithOutput(command...)
54+
if err != nil {
55+
err := fmt.Errorf("Failed to obtain VM defaults: %s", err)
56+
log.Println(err)
57+
return multistep.ActionContinue
58+
}
59+
60+
defaults, err := parseVMInfoOutput(vminfo)
61+
if err != nil {
62+
err := fmt.Errorf("Error parsing VM defaults: %s", err)
63+
log.Println(err)
64+
return multistep.ActionContinue
65+
}
66+
67+
// Store the defaults in the state bag
68+
state.Put("vmDefaults", defaults)
69+
log.Println("VM defaults retrieved successfully.")
70+
71+
return multistep.ActionContinue
72+
}
73+
74+
func parseVMInfoOutput(output string) (map[string]string, error) {
75+
defaults := make(map[string]string)
76+
lines := strings.Split(output, "\n")
77+
for _, line := range lines {
78+
if strings.Contains(line, "=") {
79+
parts := strings.Split(line, "=")
80+
if len(parts) == 2 {
81+
key := strings.Trim(strings.TrimSpace(parts[0]), `"`)
82+
value := strings.Trim(strings.TrimSpace(parts[1]), `"`)
83+
defaults[key] = value
84+
}
85+
}
86+
}
87+
if len(defaults) == 0 {
88+
return nil, fmt.Errorf("No defaults found in VM info output")
89+
}
90+
return defaults, nil
91+
}
92+
93+
func (s *stepGetVMDefaults) Cleanup(state multistep.StateBag) {
94+
// No cleanup needed for this step
95+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
packer {
2+
required_plugins {
3+
virtualbox = {
4+
version = ">= 1.0.0"
5+
source = "github.com/hashicorp/virtualbox"
6+
}
7+
}
8+
}
9+
source "virtualbox-iso" "ubuntu_2404" {
10+
# AMD64 image
11+
guest_os_type = "Ubuntu_64"
12+
iso_url = "https://releases.ubuntu.com/24.04/ubuntu-24.04.2-live-server-amd64.iso"
13+
iso_checksum = "sha256:d6dab0c3a657988501b4bd76f1297c053df710e06e0c3aece60dead24f270b4d"
14+
# ARM64 image
15+
# guest_os_type = "Ubuntu_arm64"
16+
# iso_url = "https://cdimage.ubuntu.com/releases/24.04.2/release/ubuntu-24.04.2-live-server-arm64.iso"
17+
# iso_checksum = "sha256:9fd122eedff09dc57d66e1c29acb8d7a207e2a877e762bdf30d2c913f95f03a4"
18+
iso_interface = "virtio"
19+
ssh_username = "packer"
20+
ssh_password = "packer"
21+
shutdown_command = "echo 'packer' | sudo -S shutdown -P now"
22+
vm_name = "ubuntu_vm_test"
23+
memory = 2048
24+
cpus = 2
25+
# chipset = "armv8virtual"
26+
hard_drive_interface = "virtio"
27+
# headless = true
28+
http_directory = "./testdata/http_ubuntu_2404"
29+
ssh_timeout = "60m"
30+
boot_wait = "20s"
31+
boot_command = [
32+
"c<wait><wait><wait>",
33+
"linux /casper/vmlinuz --- autoinstall ds=\"nocloud-net;seedfrom=http://{{.HTTPIP}}:{{.HTTPPort}}/\"<enter><wait>",
34+
"initrd /casper/initrd<enter><wait>",
35+
"boot<enter>"
36+
]
37+
vboxmanage = [
38+
# Firmware
39+
["modifyvm", "{{.Name}}", "--firmware", "efi"],
40+
41+
# Input devices
42+
["modifyvm", "{{.Name}}", "--mouse", "ps2"],
43+
["modifyvm", "{{.Name}}", "--keyboard", "ps2"],
44+
45+
# Boot order
46+
["modifyvm", "{{.Name}}", "--boot1", "disk"],
47+
["modifyvm", "{{.Name}}", "--boot2", "dvd"],
48+
["modifyvm", "{{.Name}}", "--boot3", "floppy"],
49+
["modifyvm", "{{.Name}}", "--boot4", "none"],
50+
51+
# Network
52+
["modifyvm", "{{.Name}}", "--macaddress1", "080027F0F51D"],
53+
["modifyvm", "{{.Name}}", "--nat-localhostreachable1", "on"],
54+
55+
# Audio
56+
["modifyvm", "{{.Name}}", "--audioin", "off"],
57+
["modifyvm", "{{.Name}}", "--audioout", "off"],
58+
59+
# Other settings
60+
["modifyvm", "{{.Name}}", "--rtcuseutc", "on"],
61+
["modifyvm", "{{.Name}}", "--usbxhci", "on"],
62+
["modifyvm", "{{.Name}}", "--clipboard-mode", "disabled"]
63+
]
64+
}
65+
66+
build {
67+
sources = ["source.virtualbox-iso.ubuntu_2404"]
68+
}
69+

0 commit comments

Comments
 (0)