Skip to content

Commit b4775ac

Browse files
committed
Manager Interface
This is part of a long series of changes. It expands on the sentiment of the Command interface, but with the goal of handing everything as a method of the Manager interface. Why? So its easier to test and change the functionality of the package. Currently its nearly impossible for a consumer of the package to test, as everything is operated using functions, and methods on structs such as Machine. The Manager interface will contain every API call. The code is currently 100% compatible with existing version, and simply builds on the existing code. For convenience, Run() function is exposed, which can be used to run any command using the default manager, while the APIs are being migrated. Once the full migration completes, the previous functions will be deprecated and later removed. This most likely will mean v2 of the library, but it is fair distance away.
1 parent c7149ec commit b4775ac

11 files changed

+316
-107
lines changed

cmd/vbhostd/vbhostd.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package main
22

33
import (
4+
"context"
45
"flag"
56
"fmt"
67
"log"
@@ -38,9 +39,11 @@ func main() {
3839
logger.Print(msg + "\n")
3940
}
4041

42+
manager := virtualbox.NewManager()
43+
4144
var vms []string
4245
if *vm == "all" {
43-
machines, err := virtualbox.ListMachines()
46+
machines, err := manager.ListMachines(context.Background())
4447
if err != nil {
4548
panic(err)
4649
}

gen.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package virtualbox
2+
3+
//go:generate mockgen -package=virtualbox -source=vbcmd.go -destination=mockvbcmd_test.go

interface.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package virtualbox
2+
3+
import (
4+
"context"
5+
)
6+
7+
// Manager allows to get and edit every property of Virtualbox.
8+
type Manager interface {
9+
// Machine gets the machine by its name or UUID
10+
Machine(context.Context, string) (*Machine, error)
11+
12+
// ListMachines returns the list of all known machines
13+
ListMachines(context.Context) ([]*Machine, error)
14+
15+
// UpdateMachine takes in the properties of the machine and applies the
16+
// configuration
17+
UpdateMachine(context.Context, *Machine) error
18+
}

machine.go

Lines changed: 86 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import (
88
"path/filepath"
99
"strconv"
1010
"strings"
11-
"sync"
1211
"time"
1312
)
1413

@@ -212,18 +211,16 @@ func (m *Machine) Delete() error {
212211
return Manage().run("unregistervm", m.Name, "--delete")
213212
}
214213

215-
var mutex sync.Mutex
216-
217-
// GetMachine finds a machine by its name or UUID.
218-
func GetMachine(id string) (*Machine, error) {
214+
// Machine returns the current machine state based on the current state.
215+
func (m *manager) Machine(ctx context.Context, id string) (*Machine, error) {
219216
/* There is a strage behavior where running multiple instances of
220217
'VBoxManage showvminfo' on same VM simultaneously can return an error of
221218
'object is not ready (E_ACCESSDENIED)', so we sequential the operation with a mutex.
222219
Note if you are running multiple process of go-virtualbox or 'showvminfo'
223220
in the command line side by side, this not gonna work. */
224-
mutex.Lock()
225-
stdout, stderr, err := Manage().runOutErr("showvminfo", id, "--machinereadable")
226-
mutex.Unlock()
221+
m.lock.Lock()
222+
stdout, stderr, err := m.run(ctx, "showvminfo", id, "--machinereadable")
223+
m.lock.Unlock()
227224
if err != nil {
228225
if reMachineNotFound.FindString(stderr) != "" {
229226
return nil, ErrMachineNotExist
@@ -251,28 +248,28 @@ func GetMachine(id string) (*Machine, error) {
251248
}
252249

253250
/* Extract basic info */
254-
m := New()
255-
m.Name = propMap["name"]
256-
m.Firmware = propMap["firmware"]
257-
m.UUID = propMap["UUID"]
258-
m.State = MachineState(propMap["VMState"])
251+
vm := New()
252+
vm.Name = propMap["name"]
253+
vm.Firmware = propMap["firmware"]
254+
vm.UUID = propMap["UUID"]
255+
vm.State = MachineState(propMap["VMState"])
259256
n, err := strconv.ParseUint(propMap["memory"], 10, 32)
260257
if err != nil {
261258
return nil, err
262259
}
263-
m.Memory = uint(n)
260+
vm.Memory = uint(n)
264261
n, err = strconv.ParseUint(propMap["cpus"], 10, 32)
265262
if err != nil {
266263
return nil, err
267264
}
268-
m.CPUs = uint(n)
265+
vm.CPUs = uint(n)
269266
n, err = strconv.ParseUint(propMap["vram"], 10, 32)
270267
if err != nil {
271268
return nil, err
272269
}
273-
m.VRAM = uint(n)
274-
m.CfgFile = propMap["CfgFile"]
275-
m.BaseFolder = filepath.Dir(m.CfgFile)
270+
vm.VRAM = uint(n)
271+
vm.CfgFile = propMap["CfgFile"]
272+
vm.BaseFolder = filepath.Dir(vm.CfgFile)
276273

277274
/* Extract NIC info */
278275
for i := 1; i <= 4; i++ {
@@ -295,29 +292,46 @@ func GetMachine(id string) (*Machine, error) {
295292
} else if nic.Network == NICNetBridged {
296293
nic.HostInterface = propMap[fmt.Sprintf("bridgeadapter%d", i)]
297294
}
298-
m.NICs = append(m.NICs, nic)
295+
vm.NICs = append(vm.NICs, nic)
299296
}
300297

301298
if err := s.Err(); err != nil {
302299
return nil, err
303300
}
304-
return m, nil
301+
return vm, nil
302+
}
303+
304+
// GetMachine finds a machine by its name or UUID.
305+
//
306+
// Deprecated: Use Manager.Machine()
307+
func GetMachine(id string) (*Machine, error) {
308+
return defaultManager.Machine(context.Background(), id)
305309
}
306310

307311
// ListMachines lists all registered machines.
312+
//
313+
// Deprecated: Use Manager.ListMachines()
308314
func ListMachines() ([]*Machine, error) {
309-
out, err := Manage().runOut("list", "vms")
315+
return defaultManager.ListMachines(context.Background())
316+
}
317+
318+
// ListMachines lists all registered machines.
319+
func (m *manager) ListMachines(ctx context.Context) ([]*Machine, error) {
320+
m.lock.Lock()
321+
out, _, err := m.run(ctx, "list", "vms")
322+
m.lock.Unlock()
310323
if err != nil {
311324
return nil, err
312325
}
326+
313327
ms := []*Machine{}
314328
s := bufio.NewScanner(strings.NewReader(out))
315329
for s.Scan() {
316330
res := reVMNameUUID.FindStringSubmatch(s.Text())
317331
if res == nil {
318332
continue
319333
}
320-
m, err := GetMachine(res[1])
334+
m, err := m.Machine(ctx, res[1])
321335
if err != nil {
322336
// Sometimes a VM is listed but not available, so we need to handle this.
323337
if err == ErrMachineNotExist {
@@ -368,44 +382,44 @@ func CreateMachine(name, basefolder string) (*Machine, error) {
368382
return m, nil
369383
}
370384

371-
// Modify changes the settings of the machine.
372-
func (m *Machine) Modify() error {
373-
args := []string{"modifyvm", m.Name,
374-
"--firmware", m.Firmware,
385+
// UpdateMachine updates the machine details based on the struct fields.
386+
func (m *manager) UpdateMachine(ctx context.Context, vm *Machine) error {
387+
args := []string{"modifyvm", vm.Name,
388+
"--firmware", vm.Firmware,
375389
"--bioslogofadein", "off",
376390
"--bioslogofadeout", "off",
377391
"--bioslogodisplaytime", "0",
378392
"--biosbootmenu", "disabled",
379393

380-
"--ostype", m.OSType,
381-
"--cpus", fmt.Sprintf("%d", m.CPUs),
382-
"--memory", fmt.Sprintf("%d", m.Memory),
383-
"--vram", fmt.Sprintf("%d", m.VRAM),
384-
385-
"--acpi", m.Flag.Get(ACPI),
386-
"--ioapic", m.Flag.Get(IOAPIC),
387-
"--rtcuseutc", m.Flag.Get(RTCUSEUTC),
388-
"--cpuhotplug", m.Flag.Get(CPUHOTPLUG),
389-
"--pae", m.Flag.Get(PAE),
390-
"--longmode", m.Flag.Get(LONGMODE),
391-
"--hpet", m.Flag.Get(HPET),
392-
"--hwvirtex", m.Flag.Get(HWVIRTEX),
393-
"--triplefaultreset", m.Flag.Get(TRIPLEFAULTRESET),
394-
"--nestedpaging", m.Flag.Get(NESTEDPAGING),
395-
"--largepages", m.Flag.Get(LARGEPAGES),
396-
"--vtxvpid", m.Flag.Get(VTXVPID),
397-
"--vtxux", m.Flag.Get(VTXUX),
398-
"--accelerate3d", m.Flag.Get(ACCELERATE3D),
399-
}
400-
401-
for i, dev := range m.BootOrder {
394+
"--ostype", vm.OSType,
395+
"--cpus", fmt.Sprintf("%d", vm.CPUs),
396+
"--memory", fmt.Sprintf("%d", vm.Memory),
397+
"--vram", fmt.Sprintf("%d", vm.VRAM),
398+
399+
"--acpi", vm.Flag.Get(ACPI),
400+
"--ioapic", vm.Flag.Get(IOAPIC),
401+
"--rtcuseutc", vm.Flag.Get(RTCUSEUTC),
402+
"--cpuhotplug", vm.Flag.Get(CPUHOTPLUG),
403+
"--pae", vm.Flag.Get(PAE),
404+
"--longmode", vm.Flag.Get(LONGMODE),
405+
"--hpet", vm.Flag.Get(HPET),
406+
"--hwvirtex", vm.Flag.Get(HWVIRTEX),
407+
"--triplefaultreset", vm.Flag.Get(TRIPLEFAULTRESET),
408+
"--nestedpaging", vm.Flag.Get(NESTEDPAGING),
409+
"--largepages", vm.Flag.Get(LARGEPAGES),
410+
"--vtxvpid", vm.Flag.Get(VTXVPID),
411+
"--vtxux", vm.Flag.Get(VTXUX),
412+
"--accelerate3d", vm.Flag.Get(ACCELERATE3D),
413+
}
414+
415+
for i, dev := range vm.BootOrder {
402416
if i > 3 {
403417
break // Only four slots `--boot{1,2,3,4}`. Ignore the rest.
404418
}
405419
args = append(args, fmt.Sprintf("--boot%d", i+1), dev)
406420
}
407421

408-
for i, nic := range m.NICs {
422+
for i, nic := range vm.NICs {
409423
n := i + 1
410424
args = append(args,
411425
fmt.Sprintf("--nic%d", n), string(nic.Network),
@@ -418,10 +432,14 @@ func (m *Machine) Modify() error {
418432
}
419433
}
420434

421-
if err := Manage().run(args...); err != nil {
435+
if _, _, err := m.run(ctx, args...); err != nil {
422436
return err
423437
}
424-
return m.Refresh()
438+
return vm.Refresh()
439+
}
440+
441+
func (m *Machine) Modify() error {
442+
return defaultManager.UpdateMachine(context.Background(), m)
425443
}
426444

427445
// AddNATPF adds a NAT port forarding rule to the n-th NIC with the given name.
@@ -475,22 +493,27 @@ func (m *Machine) DelStorageCtl(name string) error {
475493

476494
// AttachStorage attaches a storage medium to the named storage controller.
477495
func (m *Machine) AttachStorage(ctlName string, medium StorageMedium) error {
478-
return Manage().run("storageattach", m.Name, "--storagectl", ctlName,
496+
_, _, err := defaultManager.run(context.Background(),
497+
"storageattach", m.Name, "--storagectl", ctlName,
479498
"--port", fmt.Sprintf("%d", medium.Port),
480499
"--device", fmt.Sprintf("%d", medium.Device),
481500
"--type", string(medium.DriveType),
482501
"--medium", medium.Medium,
483502
)
503+
return err
484504
}
485505

486506
// SetExtraData attaches custom string to the VM.
487507
func (m *Machine) SetExtraData(key, val string) error {
488-
return Manage().run("setextradata", m.Name, key, val)
508+
_, _, err := defaultManager.run(context.Background(),
509+
"setextradata", m.Name, key, val)
510+
return err
489511
}
490512

491513
// GetExtraData retrieves custom string from the VM.
492514
func (m *Machine) GetExtraData(key string) (*string, error) {
493-
value, err := Manage().runOut("getextradata", m.Name, key)
515+
value, _, err := defaultManager.run(context.Background(),
516+
"getextradata", m.Name, key)
494517
if err != nil {
495518
return nil, err
496519
}
@@ -506,13 +529,19 @@ func (m *Machine) GetExtraData(key string) (*string, error) {
506529

507530
// DeleteExtraData removes custom string from the VM.
508531
func (m *Machine) DeleteExtraData(key string) error {
509-
return Manage().run("setextradata", m.Name, key)
532+
_, _, err := defaultManager.run(context.Background(),
533+
"setextradata", m.Name, key)
534+
return err
510535
}
511536

512537
// CloneMachine clones the given machine name into a new one.
513538
func CloneMachine(baseImageName string, newImageName string, register bool) error {
514539
if register {
515-
return Manage().run("clonevm", baseImageName, "--name", newImageName, "--register")
540+
_, _, err := defaultManager.run(context.Background(),
541+
"clonevm", baseImageName, "--name", newImageName, "--register")
542+
return err
516543
}
517-
return Manage().run("clonevm", baseImageName, "--name", newImageName)
544+
_, _, err := defaultManager.run(context.Background(),
545+
"clonevm", baseImageName, "--name", newImageName)
546+
return err
518547
}

manager.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package virtualbox
2+
3+
import (
4+
"context"
5+
"sync"
6+
)
7+
8+
var defaultManager = NewManager()
9+
10+
// manager implements all the functionality of the Manager, and is the default
11+
// one used.
12+
type manager struct {
13+
// Wrap around the existing code until its migrated
14+
cmd Command
15+
// lock the whole manager to only allow one action at a time
16+
// TODO: Decide is this a good idea, or should we have one mutex per
17+
// type of operation
18+
lock sync.Mutex
19+
}
20+
21+
// NewManager returns the real instance of the manager
22+
func NewManager() *manager {
23+
return &manager{
24+
cmd: Manage(),
25+
}
26+
}
27+
28+
// run is the internal function used by other commands.
29+
func (m *manager) run(ctx context.Context, args ...string) (string, string, error) {
30+
return m.cmd.runOutErrContext(ctx, args...)
31+
}
32+
33+
// Run is a helper function using the defaultManager and can be used to directly
34+
// run commands which are not exposed as part of the Manager API. It returns the
35+
// stdout, stderr and any errors which happened while executing the command.
36+
// The `VBoxManage` argument should not be specified at the beginning as it is
37+
// deducted from the environment.
38+
//
39+
// Notice: Its possible that if we ever cover the API 1:1, this function might
40+
// be deprecated and later removed.
41+
func Run(ctx context.Context, args ...string) (string, string, error) {
42+
return defaultManager.run(ctx, args...)
43+
}

mock/gen.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package mock
2+
3+
//go:generate mockgen -package=mock -source=../interface.go -destination=interface_mock.go

0 commit comments

Comments
 (0)