diff --git a/gen.go b/gen.go new file mode 100644 index 0000000..fedffa6 --- /dev/null +++ b/gen.go @@ -0,0 +1,3 @@ +package virtualbox + +//go:generate mockgen -package=virtualbox -source=vbcmd.go -destination=mockvbcmd_test.go diff --git a/interface.go b/interface.go new file mode 100644 index 0000000..a17b2d0 --- /dev/null +++ b/interface.go @@ -0,0 +1,18 @@ +package virtualbox + +import ( + "context" +) + +// Manager allows to get and edit every property of Virtualbox. +type Manager interface { + // Machine gets the machine by its name or UUID + Machine(context.Context, string) (*Machine, error) + + // ListMachines returns the list of all known machines + ListMachines(context.Context) ([]*Machine, error) + + // UpdateMachine takes in the properties of the machine and applies the + // configuration + UpdateMachine(context.Context, *Machine) error +} diff --git a/machine.go b/machine.go index 3f9b082..20ca608 100644 --- a/machine.go +++ b/machine.go @@ -2,11 +2,11 @@ package virtualbox import ( "bufio" + "context" "fmt" "path/filepath" "strconv" "strings" - "sync" "time" ) @@ -201,18 +201,16 @@ func (m *Machine) Delete() error { return Manage().run("unregistervm", m.Name, "--delete") } -var mutex sync.Mutex - -// GetMachine finds a machine by its name or UUID. -func GetMachine(id string) (*Machine, error) { +// Machine returns the current machine state based on the current state. +func (m *manager) Machine(ctx context.Context, id string) (*Machine, error) { /* There is a strage behavior where running multiple instances of 'VBoxManage showvminfo' on same VM simultaneously can return an error of 'object is not ready (E_ACCESSDENIED)', so we sequential the operation with a mutex. Note if you are running multiple process of go-virtualbox or 'showvminfo' in the command line side by side, this not gonna work. */ - mutex.Lock() - stdout, stderr, err := Manage().runOutErr("showvminfo", id, "--machinereadable") - mutex.Unlock() + m.lock.Lock() + stdout, stderr, err := m.run(ctx, "showvminfo", id, "--machinereadable") + m.lock.Unlock() if err != nil { if reMachineNotFound.FindString(stderr) != "" { return nil, ErrMachineNotExist @@ -240,27 +238,27 @@ func GetMachine(id string) (*Machine, error) { } /* Extract basic info */ - m := New() - m.Name = propMap["name"] - m.UUID = propMap["UUID"] - m.State = MachineState(propMap["VMState"]) + vm := New() + vm.Name = propMap["name"] + vm.UUID = propMap["UUID"] + vm.State = MachineState(propMap["VMState"]) n, err := strconv.ParseUint(propMap["memory"], 10, 32) if err != nil { return nil, err } - m.Memory = uint(n) + vm.Memory = uint(n) n, err = strconv.ParseUint(propMap["cpus"], 10, 32) if err != nil { return nil, err } - m.CPUs = uint(n) + vm.CPUs = uint(n) n, err = strconv.ParseUint(propMap["vram"], 10, 32) if err != nil { return nil, err } - m.VRAM = uint(n) - m.CfgFile = propMap["CfgFile"] - m.BaseFolder = filepath.Dir(m.CfgFile) + vm.VRAM = uint(n) + vm.CfgFile = propMap["CfgFile"] + vm.BaseFolder = filepath.Dir(vm.CfgFile) /* Extract NIC info */ for i := 1; i <= 4; i++ { @@ -283,21 +281,38 @@ func GetMachine(id string) (*Machine, error) { } else if nic.Network == NICNetBridged { nic.HostInterface = propMap[fmt.Sprintf("bridgeadapter%d", i)] } - m.NICs = append(m.NICs, nic) + vm.NICs = append(vm.NICs, nic) } if err := s.Err(); err != nil { return nil, err } - return m, nil + return vm, nil +} + +// GetMachine finds a machine by its name or UUID. +// +// Deprecated: Use Manager.Machine() +func GetMachine(id string) (*Machine, error) { + return defaultManager.Machine(context.Background(), id) } // ListMachines lists all registered machines. +// +// Deprecated: Use Manager.ListMachines() func ListMachines() ([]*Machine, error) { - out, err := Manage().runOut("list", "vms") + return defaultManager.ListMachines(context.Background()) +} + +// ListMachines lists all registered machines. +func (m *manager) ListMachines(ctx context.Context) ([]*Machine, error) { + m.lock.Lock() + out, _, err := m.run(ctx, "list", "vms") + m.lock.Unlock() if err != nil { return nil, err } + ms := []*Machine{} s := bufio.NewScanner(strings.NewReader(out)) for s.Scan() { @@ -305,7 +320,7 @@ func ListMachines() ([]*Machine, error) { if res == nil { continue } - m, err := GetMachine(res[1]) + m, err := m.Machine(ctx, res[1]) if err != nil { // Sometimes a VM is listed but not available, so we need to handle this. if err == ErrMachineNotExist { @@ -356,44 +371,44 @@ func CreateMachine(name, basefolder string) (*Machine, error) { return m, nil } -// Modify changes the settings of the machine. -func (m *Machine) Modify() error { - args := []string{"modifyvm", m.Name, +// UpdateMachine updates the machine details based on the struct fields. +func (m *manager) UpdateMachine(ctx context.Context, vm *Machine) error { + args := []string{"modifyvm", vm.Name, "--firmware", "bios", "--bioslogofadein", "off", "--bioslogofadeout", "off", "--bioslogodisplaytime", "0", "--biosbootmenu", "disabled", - "--ostype", m.OSType, - "--cpus", fmt.Sprintf("%d", m.CPUs), - "--memory", fmt.Sprintf("%d", m.Memory), - "--vram", fmt.Sprintf("%d", m.VRAM), - - "--acpi", m.Flag.Get(ACPI), - "--ioapic", m.Flag.Get(IOAPIC), - "--rtcuseutc", m.Flag.Get(RTCUSEUTC), - "--cpuhotplug", m.Flag.Get(CPUHOTPLUG), - "--pae", m.Flag.Get(PAE), - "--longmode", m.Flag.Get(LONGMODE), - "--hpet", m.Flag.Get(HPET), - "--hwvirtex", m.Flag.Get(HWVIRTEX), - "--triplefaultreset", m.Flag.Get(TRIPLEFAULTRESET), - "--nestedpaging", m.Flag.Get(NESTEDPAGING), - "--largepages", m.Flag.Get(LARGEPAGES), - "--vtxvpid", m.Flag.Get(VTXVPID), - "--vtxux", m.Flag.Get(VTXUX), - "--accelerate3d", m.Flag.Get(ACCELERATE3D), - } - - for i, dev := range m.BootOrder { + "--ostype", vm.OSType, + "--cpus", fmt.Sprintf("%d", vm.CPUs), + "--memory", fmt.Sprintf("%d", vm.Memory), + "--vram", fmt.Sprintf("%d", vm.VRAM), + + "--acpi", vm.Flag.Get(ACPI), + "--ioapic", vm.Flag.Get(IOAPIC), + "--rtcuseutc", vm.Flag.Get(RTCUSEUTC), + "--cpuhotplug", vm.Flag.Get(CPUHOTPLUG), + "--pae", vm.Flag.Get(PAE), + "--longmode", vm.Flag.Get(LONGMODE), + "--hpet", vm.Flag.Get(HPET), + "--hwvirtex", vm.Flag.Get(HWVIRTEX), + "--triplefaultreset", vm.Flag.Get(TRIPLEFAULTRESET), + "--nestedpaging", vm.Flag.Get(NESTEDPAGING), + "--largepages", vm.Flag.Get(LARGEPAGES), + "--vtxvpid", vm.Flag.Get(VTXVPID), + "--vtxux", vm.Flag.Get(VTXUX), + "--accelerate3d", vm.Flag.Get(ACCELERATE3D), + } + + for i, dev := range vm.BootOrder { if i > 3 { break // Only four slots `--boot{1,2,3,4}`. Ignore the rest. } args = append(args, fmt.Sprintf("--boot%d", i+1), dev) } - for i, nic := range m.NICs { + for i, nic := range vm.NICs { n := i + 1 args = append(args, fmt.Sprintf("--nic%d", n), string(nic.Network), @@ -406,10 +421,14 @@ func (m *Machine) Modify() error { } } - if err := Manage().run(args...); err != nil { + if _, _, err := m.run(ctx, args...); err != nil { return err } - return m.Refresh() + return vm.Refresh() +} + +func (m *Machine) Modify() error { + return defaultManager.UpdateMachine(context.Background(), m) } // AddNATPF adds a NAT port forarding rule to the n-th NIC with the given name. @@ -463,22 +482,27 @@ func (m *Machine) DelStorageCtl(name string) error { // AttachStorage attaches a storage medium to the named storage controller. func (m *Machine) AttachStorage(ctlName string, medium StorageMedium) error { - return Manage().run("storageattach", m.Name, "--storagectl", ctlName, + _, _, err := defaultManager.run(context.Background(), + "storageattach", m.Name, "--storagectl", ctlName, "--port", fmt.Sprintf("%d", medium.Port), "--device", fmt.Sprintf("%d", medium.Device), "--type", string(medium.DriveType), "--medium", medium.Medium, ) + return err } // SetExtraData attaches custom string to the VM. func (m *Machine) SetExtraData(key, val string) error { - return Manage().run("setextradata", m.Name, key, val) + _, _, err := defaultManager.run(context.Background(), + "setextradata", m.Name, key, val) + return err } // GetExtraData retrieves custom string from the VM. func (m *Machine) GetExtraData(key string) (*string, error) { - value, err := Manage().runOut("getextradata", m.Name, key) + value, _, err := defaultManager.run(context.Background(), + "getextradata", m.Name, key) if err != nil { return nil, err } @@ -494,13 +518,19 @@ func (m *Machine) GetExtraData(key string) (*string, error) { // DeleteExtraData removes custom string from the VM. func (m *Machine) DeleteExtraData(key string) error { - return Manage().run("setextradata", m.Name, key) + _, _, err := defaultManager.run(context.Background(), + "setextradata", m.Name, key) + return err } // CloneMachine clones the given machine name into a new one. func CloneMachine(baseImageName string, newImageName string, register bool) error { if register { - return Manage().run("clonevm", baseImageName, "--name", newImageName, "--register") + _, _, err := defaultManager.run(context.Background(), + "clonevm", baseImageName, "--name", newImageName, "--register") + return err } - return Manage().run("clonevm", baseImageName, "--name", newImageName) + _, _, err := defaultManager.run(context.Background(), + "clonevm", baseImageName, "--name", newImageName) + return err } diff --git a/manager.go b/manager.go new file mode 100644 index 0000000..85a172b --- /dev/null +++ b/manager.go @@ -0,0 +1,43 @@ +package virtualbox + +import ( + "context" + "sync" +) + +var defaultManager = NewManager() + +// manager implements all the functionality of the Manager, and is the default +// one used. +type manager struct { + // Wrap around the existing code until its migrated + cmd Command + // lock the whole manager to only allow one action at a time + // TODO: Decide is this a good idea, or should we have one mutex per + // type of operation + lock sync.Mutex +} + +// NewManager returns the real instance of the manager +func NewManager() *manager { + return &manager{ + cmd: Manage(), + } +} + +// run is the internal function used by other commands. +func (m *manager) run(ctx context.Context, args ...string) (string, string, error) { + return m.cmd.runOutErrContext(ctx, args...) +} + +// Run is a helper function using the defaultManager and can be used to directly +// run commands which are not exposed as part of the Manager API. It returns the +// stdout, stderr and any errors which happened while executing the command. +// The `VBoxManage` argument should not be specified at the beginning as it is +// deducted from the environment. +// +// Notice: Its possible that if we ever cover the API 1:1, this function might +// be deprecated and later removed. +func Run(ctx context.Context, args ...string) (string, string, error) { + return defaultManager.run(ctx, args...) +} diff --git a/mock/gen.go b/mock/gen.go new file mode 100644 index 0000000..33fac67 --- /dev/null +++ b/mock/gen.go @@ -0,0 +1,3 @@ +package mock + +//go:generate mockgen -package=mock -source=../interface.go -destination=interface_mock.go diff --git a/mock/interface_mock.go b/mock/interface_mock.go new file mode 100644 index 0000000..aa556c6 --- /dev/null +++ b/mock/interface_mock.go @@ -0,0 +1,80 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ../interface.go + +// Package mock is a generated GoMock package. +package mock + +import ( + context "context" + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + virtualbox "github.com/terra-farm/go-virtualbox" +) + +// MockManager is a mock of Manager interface. +type MockManager struct { + ctrl *gomock.Controller + recorder *MockManagerMockRecorder +} + +// MockManagerMockRecorder is the mock recorder for MockManager. +type MockManagerMockRecorder struct { + mock *MockManager +} + +// NewMockManager creates a new mock instance. +func NewMockManager(ctrl *gomock.Controller) *MockManager { + mock := &MockManager{ctrl: ctrl} + mock.recorder = &MockManagerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockManager) EXPECT() *MockManagerMockRecorder { + return m.recorder +} + +// ListMachines mocks base method. +func (m *MockManager) ListMachines(arg0 context.Context) ([]*virtualbox.Machine, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListMachines", arg0) + ret0, _ := ret[0].([]*virtualbox.Machine) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListMachines indicates an expected call of ListMachines. +func (mr *MockManagerMockRecorder) ListMachines(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListMachines", reflect.TypeOf((*MockManager)(nil).ListMachines), arg0) +} + +// Machine mocks base method. +func (m *MockManager) Machine(arg0 context.Context, arg1 string) (*virtualbox.Machine, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Machine", arg0, arg1) + ret0, _ := ret[0].(*virtualbox.Machine) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Machine indicates an expected call of Machine. +func (mr *MockManagerMockRecorder) Machine(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Machine", reflect.TypeOf((*MockManager)(nil).Machine), arg0, arg1) +} + +// UpdateMachine mocks base method. +func (m *MockManager) UpdateMachine(arg0 context.Context, arg1 *virtualbox.Machine) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateMachine", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpdateMachine indicates an expected call of UpdateMachine. +func (mr *MockManagerMockRecorder) UpdateMachine(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateMachine", reflect.TypeOf((*MockManager)(nil).UpdateMachine), arg0, arg1) +} diff --git a/mockvbcmd_test.go b/mockvbcmd_test.go index 82f2e5a..76980a0 100644 --- a/mockvbcmd_test.go +++ b/mockvbcmd_test.go @@ -5,75 +5,66 @@ package virtualbox import ( - gomock "github.com/golang/mock/gomock" + context "context" reflect "reflect" + + gomock "github.com/golang/mock/gomock" ) -// MockCommand is a mock of Command interface +// MockCommand is a mock of Command interface. type MockCommand struct { ctrl *gomock.Controller recorder *MockCommandMockRecorder } -// MockCommandMockRecorder is the mock recorder for MockCommand +// MockCommandMockRecorder is the mock recorder for MockCommand. type MockCommandMockRecorder struct { mock *MockCommand } -// NewMockCommand creates a new mock instance +// NewMockCommand creates a new mock instance. func NewMockCommand(ctrl *gomock.Controller) *MockCommand { mock := &MockCommand{ctrl: ctrl} mock.recorder = &MockCommandMockRecorder{mock} return mock } -// EXPECT returns an object that allows the caller to indicate expected use +// EXPECT returns an object that allows the caller to indicate expected use. func (m *MockCommand) EXPECT() *MockCommandMockRecorder { return m.recorder } -// setOpts mocks base method -func (m *MockCommand) setOpts(opts ...option) Command { - varargs := []interface{}{} - for _, a := range opts { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "setOpts", varargs...) - ret0, _ := ret[0].(Command) - return ret0 -} - -// setOpts indicates an expected call of setOpts -func (mr *MockCommandMockRecorder) setOpts(opts ...interface{}) *gomock.Call { - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "setOpts", reflect.TypeOf((*MockCommand)(nil).setOpts), opts...) -} - -// isGuest mocks base method +// isGuest mocks base method. func (m *MockCommand) isGuest() bool { + m.ctrl.T.Helper() ret := m.ctrl.Call(m, "isGuest") ret0, _ := ret[0].(bool) return ret0 } -// isGuest indicates an expected call of isGuest +// isGuest indicates an expected call of isGuest. func (mr *MockCommandMockRecorder) isGuest() *gomock.Call { + mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "isGuest", reflect.TypeOf((*MockCommand)(nil).isGuest)) } -// path mocks base method +// path mocks base method. func (m *MockCommand) path() string { + m.ctrl.T.Helper() ret := m.ctrl.Call(m, "path") ret0, _ := ret[0].(string) return ret0 } -// path indicates an expected call of path +// path indicates an expected call of path. func (mr *MockCommandMockRecorder) path() *gomock.Call { + mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "path", reflect.TypeOf((*MockCommand)(nil).path)) } -// run mocks base method +// run mocks base method. func (m *MockCommand) run(args ...string) error { + m.ctrl.T.Helper() varargs := []interface{}{} for _, a := range args { varargs = append(varargs, a) @@ -83,13 +74,15 @@ func (m *MockCommand) run(args ...string) error { return ret0 } -// run indicates an expected call of run +// run indicates an expected call of run. func (mr *MockCommandMockRecorder) run(args ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "run", reflect.TypeOf((*MockCommand)(nil).run), args...) } -// runOut mocks base method +// runOut mocks base method. func (m *MockCommand) runOut(args ...string) (string, error) { + m.ctrl.T.Helper() varargs := []interface{}{} for _, a := range args { varargs = append(varargs, a) @@ -100,13 +93,15 @@ func (m *MockCommand) runOut(args ...string) (string, error) { return ret0, ret1 } -// runOut indicates an expected call of runOut +// runOut indicates an expected call of runOut. func (mr *MockCommandMockRecorder) runOut(args ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "runOut", reflect.TypeOf((*MockCommand)(nil).runOut), args...) } -// runOutErr mocks base method +// runOutErr mocks base method. func (m *MockCommand) runOutErr(args ...string) (string, string, error) { + m.ctrl.T.Helper() varargs := []interface{}{} for _, a := range args { varargs = append(varargs, a) @@ -118,7 +113,47 @@ func (m *MockCommand) runOutErr(args ...string) (string, string, error) { return ret0, ret1, ret2 } -// runOutErr indicates an expected call of runOutErr +// runOutErr indicates an expected call of runOutErr. func (mr *MockCommandMockRecorder) runOutErr(args ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "runOutErr", reflect.TypeOf((*MockCommand)(nil).runOutErr), args...) } + +// runOutErrContext mocks base method. +func (m *MockCommand) runOutErrContext(ctx context.Context, args ...string) (string, string, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx} + for _, a := range args { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "runOutErrContext", varargs...) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(string) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// runOutErrContext indicates an expected call of runOutErrContext. +func (mr *MockCommandMockRecorder) runOutErrContext(ctx interface{}, args ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx}, args...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "runOutErrContext", reflect.TypeOf((*MockCommand)(nil).runOutErrContext), varargs...) +} + +// setOpts mocks base method. +func (m *MockCommand) setOpts(opts ...option) Command { + m.ctrl.T.Helper() + varargs := []interface{}{} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "setOpts", varargs...) + ret0, _ := ret[0].(Command) + return ret0 +} + +// setOpts indicates an expected call of setOpts. +func (mr *MockCommandMockRecorder) setOpts(opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "setOpts", reflect.TypeOf((*MockCommand)(nil).setOpts), opts...) +} diff --git a/vbcmd.go b/vbcmd.go index f01e2d6..523906b 100644 --- a/vbcmd.go +++ b/vbcmd.go @@ -2,6 +2,7 @@ package virtualbox import ( "bytes" + "context" "errors" "os" "os/exec" @@ -19,6 +20,7 @@ type Command interface { run(args ...string) error runOut(args ...string) (string, error) runOutErr(args ...string) (string, string, error) + runOutErrContext(ctx context.Context, args ...string) (string, string, error) } var ( @@ -63,7 +65,7 @@ func (vbcmd command) path() string { return vbcmd.program } -func (vbcmd command) prepare(args []string) *exec.Cmd { +func (vbcmd command) prepare(ctx context.Context, args []string) *exec.Cmd { program := vbcmd.program argv := []string{} Debug("Command: '%+v', runtime.GOOS: '%s'", vbcmd, runtime.GOOS) @@ -73,12 +75,12 @@ func (vbcmd command) prepare(args []string) *exec.Cmd { } argv = append(argv, args...) Debug("executing: %v %v", program, argv) - return exec.Command(program, argv...) // #nosec + return exec.CommandContext(ctx, program, argv...) // #nosec } func (vbcmd command) run(args ...string) error { defer vbcmd.setOpts(sudo(false)) - cmd := vbcmd.prepare(args) + cmd := vbcmd.prepare(context.Background(), args) if Verbose { cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr @@ -94,7 +96,7 @@ func (vbcmd command) run(args ...string) error { func (vbcmd command) runOut(args ...string) (string, error) { defer vbcmd.setOpts(sudo(false)) - cmd := vbcmd.prepare(args) + cmd := vbcmd.prepare(context.Background(), args) if Verbose { cmd.Stderr = os.Stderr } @@ -108,9 +110,9 @@ func (vbcmd command) runOut(args ...string) (string, error) { return string(b), err } -func (vbcmd command) runOutErr(args ...string) (string, string, error) { +func (vbcmd command) runOutErrContext(ctx context.Context, args ...string) (string, string, error) { defer vbcmd.setOpts(sudo(false)) - cmd := vbcmd.prepare(args) + cmd := vbcmd.prepare(ctx, args) var stdout bytes.Buffer var stderr bytes.Buffer cmd.Stdout = &stdout @@ -123,3 +125,7 @@ func (vbcmd command) runOutErr(args ...string) (string, string, error) { } return stdout.String(), stderr.String(), err } + +func (vbcmd command) runOutErr(args ...string) (string, string, error) { + return vbcmd.runOutErrContext(context.Background(), args...) +}