├── .github └── workflows │ └── continuous-integration.yml ├── .gitignore ├── LICENSE ├── README.md ├── Vagrantfile ├── cmd └── vbhostd │ ├── README.md │ ├── open_darwin.go │ ├── open_linux.go │ ├── open_windows.go │ └── vbhostd.go ├── const.go ├── dhcp.go ├── dhcp_test.go ├── disk.go ├── doc.go ├── example_guestprop_test.go ├── extra.go ├── go.mod ├── go.sum ├── guestprop.go ├── guestprop_test.go ├── hostonlynet.go ├── hostonlynet_test.go ├── import.go ├── init_test.go ├── interface.go ├── log.go ├── log_test.go ├── machine.go ├── machine_test.go ├── manager.go ├── manager_test.go ├── natnet.go ├── natnet_test.go ├── nic.go ├── pfrule.go ├── regexp_test.go ├── storage.go ├── testdata ├── list_vms.out ├── showvminfo_Ubuntu_--machinereadable.out ├── showvminfo_go-virtualbox_--machinereadable.out ├── vboxmanage-guestproperty-wait-1.out ├── vboxmanage-guestproperty-wait-2.out ├── vboxmanage-list-dhcpservers-1.out ├── vboxmanage-list-hostonlyifs-1.out ├── vboxmanage-list-natnets-1.out ├── vboxmanage-list-vms-1.out ├── vboxmanage-showvminfo-1.out └── vboxmanage-showvminfo-2.out ├── util.go ├── vbcmd.go ├── vbcmd.mock.go ├── vbmgt.go └── vbmgt_test.go /.github/workflows/continuous-integration.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | 3 | on: [push] 4 | 5 | jobs: 6 | vendor: 7 | name: Vendor 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/setup-go@v3 11 | - uses: actions/checkout@v3 12 | - uses: actions/cache@v3 13 | with: 14 | path: | 15 | ~/.cache/go-build 16 | ~/go/pkg/mod 17 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 18 | restore-keys: | 19 | ${{ runner.os }}-go- 20 | - run: go mod download 21 | 22 | test: 23 | name: Test 24 | needs: vendor 25 | runs-on: ubuntu-latest 26 | strategy: 27 | matrix: 28 | go: 29 | # TODO: Figure out can this be extracted into a common variable at 30 | # the top of the file, so its easier to update. 31 | - "1.19" 32 | - "1.18" 33 | steps: 34 | - uses: actions/setup-go@v3 35 | with: 36 | go-version: ${{ matrix.go }} 37 | - uses: actions/checkout@v3 38 | - uses: actions/cache@v3 39 | with: 40 | path: | 41 | ~/.cache/go-build 42 | ~/go/pkg/mod 43 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 44 | restore-keys: | 45 | ${{ runner.os }}-go- 46 | - run: go test ./... 47 | 48 | 49 | lint: 50 | name: Lint 51 | needs: vendor 52 | runs-on: ubuntu-latest 53 | steps: 54 | - uses: actions/setup-go@v3 55 | - uses: actions/checkout@v3 56 | - uses: golangci/golangci-lint-action@v3 57 | 58 | build: 59 | name: Build 60 | needs: vendor 61 | strategy: 62 | matrix: 63 | go: 64 | - "1.19" 65 | - "1.18" 66 | runs-on: ubuntu-latest 67 | steps: 68 | - uses: actions/setup-go@v3 69 | with: 70 | go-version: ${{ matrix.go }} 71 | - uses: actions/checkout@v3 72 | - uses: actions/cache@v3 73 | with: 74 | path: | 75 | ~/.cache/go-build 76 | ~/go/pkg/mod 77 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 78 | restore-keys: | 79 | ${{ runner.os }}-go- 80 | - run: go build ./... 81 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage.txt 2 | /.vagrant 3 | /vendor 4 | /godoc* 5 | /vbhostd* 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-virtualbox 2 | 3 | This is a wrapper package for Golang to interact with VirtualBox. The API is 4 | experimental at the moment and you should expect frequent changes. There is 5 | no compatibility guarantee between newer and older versions of Virtualbox. 6 | 7 | **Table of Contents** 8 | 9 | 10 | 11 | 1. [Status](#status) 12 | 2. [Usage](#usage) 13 | 1. [Library](#library) 14 | 2. [Commands](#commands) 15 | 3. [Documentation](#documentation) 16 | 3. [Building](#building) 17 | 4. [Testing](#testing) 18 | 1. [Preparation](#preparation) 19 | 2. [Run tests](#run-tests) 20 | 3. [Re-generate mock](#re-generate-mock) 21 | 5. [Caveats](#caveats) 22 | 23 | 24 | 25 | ## Status 26 | 27 | | Project | Status | Notes | 28 | |---------|--------|-------| 29 | | [Github Actions](https://github.com/features/actions) | [![Continuous Integration](https://github.com/terra-farm/go-virtualbox/workflows/Continuous%20Integration/badge.svg)](https://github.com/terra-farm/go-virtualbox/actions) | | 30 | | [Go Report Card](https://goreportcard.com/) | [![Go Report Card](https://goreportcard.com/badge/github.com/terra-farm/go-virtualbox?style=flat-square)](https://goreportcard.com/report/github.com/terra-farm/go-virtualbox) | scan code with `gofmt`, `go vet`, `gocyclo`, `golint`, `ineffassign`, `license` and `misspell`. | 31 | | [GoDoc](http://godoc.org) | [![Go Doc](https://img.shields.io/badge/godoc-reference-blue.svg?style=flat-square)](http://godoc.org/github.com/terra-farm/go-virtualbox) | | 32 | | Release | [![Release](https://img.shields.io/github/release/terra-farm/go-virtualbox.svg?style=flat-square)](https://github.com/terra-farm/go-virtualbox/releases/latest) | | 33 | 34 | ## Usage 35 | 36 | ### Library 37 | 38 | The part of the library that manages guest properties can run both from the Host and the Guest. 39 | 40 | ```go 41 | err := virtualbox.SetGuestProperty("MyVM", "test_key", "test_val") 42 | val, err := GetGuestProperty(VM, "test_key") 43 | ``` 44 | 45 | See [GoDoc](https://godoc.org/github.com/terra-farm/go-virtualbox) for full details. 46 | 47 | ### Commands 48 | 49 | The [vbhostd](./cmd/vbhostd/README.md) commands waits on the `vbhostd/*` guest-properties pattern. 50 | 51 | - When the guest writes a value on the `vbhostd/open`, it causes the host to open the given location: 52 | - Write `http://www.hp.com` will open the default browser as the given URL 53 | - Write `mailto:foo@bar.com?Cc=bar@foo.com` opens the default mailer pre-filling the recipient and carbon-copy recipient fields 54 | 55 | ### Documentation 56 | 57 | For the released version, see [GoDoc:terra-farm/go-virtualbox](https://godoc.org/github.com/terra-farm/go-virtualbox). To see the local documentation, run `godoc -http=:6060 &` and then `open http://localhost:6060/pkg/github.com/terra-farm/go-virtualbox/`. 58 | 59 | ## Building 60 | 61 | First install dependencies 62 | 63 | - [GoLang](https://golang.org/doc/install#install) 64 | - [GNU Make](https://www.gnu.org/software/make/) (Windows: via `choco install -y gnuwin32-make.portable) 65 | 66 | Get Go dependencies: `make deps` or: 67 | 68 | ```bash 69 | $ go get -v github.com/golang/dep/cmd/dep 70 | $ dep ensure -v 71 | ``` 72 | 73 | Then build: `make build` or: 74 | 75 | ```bash 76 | $ go build -v ./... 77 | ``` 78 | 79 | * `default` run everything in order 80 | * `deps` install dependencies (`dep` and others) 81 | * `build` run `go build ./...` 82 | * `test` run `go test ./...` 83 | * `lint` only run `gometalinter` linter 84 | 85 | ## Testing 86 | 87 | ### Preparation 88 | 89 | Tests run using mocked interfaces, unless the `TEST_VM` environment variable is set, in order to test against real VirtualBox. You either need to have a pre-provisioned VirtualBox VM and to set its name using the `TEST_VM` environment variable, or use [Vagrant](https://www.vagrantup.com/intro/getting-started/). 90 | 91 | ```bash 92 | $ vagrant box add bento/ubuntu-16.04 93 | # select virtualbox as provider 94 | 95 | $ vagrant up 96 | ``` 97 | 98 | Then run the tests 99 | 100 | ```bash 101 | $ export TEST_VM=go-virtualbox 102 | $ go test 103 | ``` 104 | 105 | ...or (on Windows): 106 | 107 | ```shell 108 | > set TEST_VM=go-virtualbox 109 | > go test 110 | ``` 111 | 112 | Once you are done with testing, run `vagrant halt` to same resources. 113 | 114 | ### Run tests 115 | 116 | As usual, run `go test`, or `go test -v`. To run one test in particular, 117 | run `go test --run TestGuestProperty`. 118 | 119 | 120 | 121 | ### Re-generate mock 122 | 123 | ```bash 124 | go generate ./... 125 | ``` 126 | 127 | ## Using local changes with your own project 128 | 129 | If you have a project which depends on this library, you probably want to test your local changes of `go-virtualbox` in your project. 130 | See [this article](https://medium.com/@teivah/how-to-test-a-local-branch-with-go-mod-54df087fc9cc) on how to set up your environment 131 | to do this. 132 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | gosrc = File.join(ENV['GOPATH'] || File.join((ENV['HOME'] || ENV['USERPROFILE']), "go"),"src") 5 | 6 | Vagrant.configure("2") do |config| 7 | 8 | config.vm.box = "bento/ubuntu-16.04" 9 | 10 | #config.vm.define 'go-virtualbox' 11 | #config.vm.hostname = 'go-virtualbox' 12 | 13 | config.vm.synced_folder Dir.home, '/home/vagrant/home', create: true 14 | config.vm.synced_folder gosrc, '/home/vagrant/GO/src', create: true 15 | 16 | #config.vm.provision 'file', source: 'golang-bashrc', destination: '~/.bashrc' 17 | config.vm.provision :shell, path: "golang-bootstrap.sh" 18 | 19 | config.vm.provider :virtualbox do |vb| 20 | vb.name = 'go-virtualbox' 21 | end 22 | 23 | end 24 | -------------------------------------------------------------------------------- /cmd/vbhostd/README.md: -------------------------------------------------------------------------------- 1 | VirtualBox Host Daemon 2 | ====================== 3 | 4 | `vboxhostd` executes commands passed via VirtualBox guest properties on the host side. 5 | 6 | | Commands | Description | 7 | |----------|-------------| 8 | | open | Run `open` (macOS), `xdg_open` (Linux) or `start` (Windows XXX not implemented) with the given value, which is supposed to be an URL (starting by `http:`, `https:` or `mailto:`) | 9 | | | | 10 | 11 | Usage: 12 | 13 | - On host, start `vbhostd` 14 | - On host, run `VBoxManage guestproperty set go-virtualbox vbhostd/open http://www.apple.com`, to open the default browser at http://www.apple.com. 15 | - In Linux/macOS guest, run `sudo VBoxControl guestproperty set vbhostd/open http://www.hp.com`, to open the default browser at http://www.hp.com. 16 | -------------------------------------------------------------------------------- /cmd/vbhostd/open_darwin.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os/exec" 5 | ) 6 | 7 | func open(args ...string) *exec.Cmd { 8 | return exec.Command("open", args...) // #nosec 9 | } 10 | -------------------------------------------------------------------------------- /cmd/vbhostd/open_linux.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os/exec" 5 | ) 6 | 7 | func open(args ...string) *exec.Cmd { 8 | return exec.Command("xdg_open", args...) // #nosec 9 | } 10 | -------------------------------------------------------------------------------- /cmd/vbhostd/open_windows.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os/exec" 5 | 6 | "github.com/terra-farm/go-virtualbox" 7 | ) 8 | 9 | func open(args ...string) *exec.Cmd { 10 | argv := append([]string{"/c"}, "start") 11 | argv = append(argv, args...) 12 | virtualbox.Debug("Executing %v %v", "cmd", argv) 13 | return exec.Command("cmd", argv...) // #nosec 14 | } 15 | -------------------------------------------------------------------------------- /cmd/vbhostd/vbhostd.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "os" 8 | "regexp" 9 | "strings" 10 | "sync" 11 | "time" 12 | 13 | "github.com/terra-farm/go-virtualbox" 14 | ) 15 | 16 | var ( 17 | openRegexp = regexp.MustCompile("^(http|https|mailto):") 18 | ) 19 | 20 | func main() { 21 | vm := flag.String("vm", "all", "VM to wait events from (all)") 22 | verbose := flag.Bool("v", false, "run in verbose mode") 23 | help := flag.Bool("h", false, "this message") 24 | flag.Parse() 25 | if *help { 26 | flag.PrintDefaults() 27 | os.Exit(0) 28 | } 29 | 30 | logger := log.New(os.Stderr, "", 0) 31 | virtualbox.Verbose = *verbose 32 | virtualbox.Debug = func(format string, args ...interface{}) { 33 | if !*verbose { 34 | return 35 | } 36 | msg := fmt.Sprintf(format, args...) 37 | logger.SetPrefix("\t ") 38 | logger.Print(msg + "\n") 39 | } 40 | 41 | var vms []string 42 | if *vm == "all" { 43 | machines, err := virtualbox.ListMachines() 44 | if err != nil { 45 | panic(err) 46 | } 47 | if *verbose { 48 | virtualbox.Debug("machines: %+v\n", machines) 49 | } 50 | for _, machine := range machines { 51 | vms = append(vms, machine.Name) 52 | } 53 | if *verbose { 54 | virtualbox.Debug("vms: %+v\n", vms) 55 | } 56 | } else { 57 | vms = append(vms, *vm) 58 | } 59 | 60 | wg := new(sync.WaitGroup) 61 | agg := make(chan virtualbox.GuestProperty) 62 | done := make(map[string]chan bool) 63 | 64 | for _, vm := range vms { 65 | done[vm] = make(chan bool) 66 | props := virtualbox.WaitGuestProperties(vm, "vbhostd/*", done[vm], wg) 67 | go func(c chan virtualbox.GuestProperty) { 68 | for prop := range c { 69 | agg <- prop 70 | } 71 | }(props) 72 | } 73 | 74 | func() { 75 | for prop := range agg { 76 | virtualbox.Debug("Got prop: %+v.\n", prop) 77 | switch prop.Name { 78 | case "vbhostd/open": 79 | fmt.Printf("opening: %v\n", prop.Value) 80 | virtualbox.Debug("opening: %v", prop.Value) 81 | if openRegexp.MatchString(prop.Value) { 82 | args := strings.Split(prop.Value, " ") 83 | cmd := open(args...) 84 | err := cmd.Run() 85 | if err != nil { 86 | fmt.Printf("Error: %v\n", err) 87 | } 88 | } else { 89 | fmt.Printf("Error: not a supported URL=%v\n", prop.Value) 90 | virtualbox.Debug("Error: not a supported URL=%v", prop.Value) 91 | } 92 | case "vbhostd/error": 93 | fmt.Printf("Error: %v\n", prop.Value) 94 | virtualbox.Debug("Error: %v", prop.Value) 95 | return 96 | case "": 97 | fmt.Printf("Unexpected error: %v\n", prop.Value) 98 | virtualbox.Debug("Unexpected error: %v", prop.Value) 99 | return 100 | } 101 | } 102 | }() 103 | 104 | for vm, d := range done { 105 | virtualbox.Debug("Closing WaitGuestProperties(%s)...\n", vm) 106 | close(d) 107 | virtualbox.Debug("Closing WaitGuestProperties(%s)... Ok\n", vm) 108 | } 109 | 110 | virtualbox.Debug("Waiting completion or timeout...\n") 111 | wait := make(chan struct{}) 112 | go func() { 113 | wg.Wait() 114 | close(wait) 115 | }() 116 | 117 | select { 118 | case <-wait: 119 | virtualbox.Debug("Every WaitGuestProperties() have completed.\n") 120 | case <-time.After(2000 * time.Millisecond): 121 | virtualbox.Debug("Timeout.\n") 122 | } 123 | 124 | fmt.Printf("Exiting....\n") 125 | virtualbox.Debug("Exiting....\n") 126 | } 127 | -------------------------------------------------------------------------------- /const.go: -------------------------------------------------------------------------------- 1 | package virtualbox 2 | 3 | const ( 4 | stringYes string = "Yes" 5 | osWindows string = "windows" 6 | ) 7 | -------------------------------------------------------------------------------- /dhcp.go: -------------------------------------------------------------------------------- 1 | package virtualbox 2 | 3 | import ( 4 | "bufio" 5 | "net" 6 | "strings" 7 | ) 8 | 9 | // DHCP server info. 10 | type DHCP struct { 11 | NetworkName string 12 | IPv4 net.IPNet 13 | LowerIP net.IP 14 | UpperIP net.IP 15 | Enabled bool 16 | } 17 | 18 | func addDHCP(kind, name string, d DHCP) error { 19 | args := []string{"dhcpserver", "add", 20 | kind, name, 21 | "--ip", d.IPv4.IP.String(), 22 | "--netmask", net.IP(d.IPv4.Mask).String(), 23 | "--lowerip", d.LowerIP.String(), 24 | "--upperip", d.UpperIP.String(), 25 | } 26 | if d.Enabled { 27 | args = append(args, "--enable") 28 | } else { 29 | args = append(args, "--disable") 30 | } 31 | _, _, err := Manage().run(args...) 32 | return err 33 | } 34 | 35 | // AddInternalDHCP adds a DHCP server to an internal network. 36 | func AddInternalDHCP(netname string, d DHCP) error { 37 | return addDHCP("--netname", netname, d) 38 | } 39 | 40 | // AddHostonlyDHCP adds a DHCP server to a host-only network. 41 | func AddHostonlyDHCP(ifname string, d DHCP) error { 42 | return addDHCP("--ifname", ifname, d) 43 | } 44 | 45 | // DHCPs gets all DHCP server settings in a map keyed by DHCP.NetworkName. 46 | func DHCPs() (map[string]*DHCP, error) { 47 | out, _, err := Manage().run("list", "dhcpservers") 48 | if err != nil { 49 | return nil, err 50 | } 51 | s := bufio.NewScanner(strings.NewReader(out)) 52 | m := map[string]*DHCP{} 53 | dhcp := &DHCP{} 54 | for s.Scan() { 55 | line := s.Text() 56 | if line == "" { 57 | m[dhcp.NetworkName] = dhcp 58 | dhcp = &DHCP{} 59 | continue 60 | } 61 | res := reColonLine.FindStringSubmatch(line) 62 | if res == nil { 63 | continue 64 | } 65 | switch key, val := res[1], res[2]; key { 66 | case "NetworkName": 67 | dhcp.NetworkName = val 68 | case "IP": 69 | dhcp.IPv4.IP = net.ParseIP(val) 70 | case "upperIPAddress": 71 | dhcp.UpperIP = net.ParseIP(val) 72 | case "lowerIPAddress": 73 | dhcp.LowerIP = net.ParseIP(val) 74 | case "NetworkMask": 75 | dhcp.IPv4.Mask = ParseIPv4Mask(val) 76 | case "Enabled": 77 | dhcp.Enabled = (val == stringYes) 78 | } 79 | } 80 | if err := s.Err(); err != nil { 81 | return nil, err 82 | } 83 | return m, nil 84 | } 85 | -------------------------------------------------------------------------------- /dhcp_test.go: -------------------------------------------------------------------------------- 1 | package virtualbox 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/golang/mock/gomock" 7 | ) 8 | 9 | func TestDHCPs(t *testing.T) { 10 | Setup(t) 11 | 12 | if ManageMock != nil { 13 | listDhcpServersOut := ReadTestData("vboxmanage-list-dhcpservers-1.out") 14 | gomock.InOrder( 15 | ManageMock.EXPECT().run("list", "dhcpservers").Return(listDhcpServersOut, "", nil).Times(1), 16 | ) 17 | } 18 | m, err := DHCPs() 19 | if err != nil { 20 | t.Fatal(err) 21 | } 22 | 23 | for _, dhcp := range m { 24 | t.Logf("%+v", dhcp) 25 | } 26 | 27 | Teardown() 28 | } 29 | -------------------------------------------------------------------------------- /disk.go: -------------------------------------------------------------------------------- 1 | package virtualbox 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "os/exec" 8 | ) 9 | 10 | // MakeDiskImage makes a disk image at dest with the given size in MB. If r is 11 | // not nil, it will be read as a raw disk image to convert from. 12 | func MakeDiskImage(dest string, size uint, r io.Reader) error { 13 | // Convert a raw image from stdin to the dest VDI image. 14 | sizeBytes := int64(size) << 20 // usually won't fit in 32-bit int (max 2GB) 15 | cmd := exec.Command(Manage().path(), "convertfromraw", "stdin", dest, 16 | fmt.Sprintf("%d", sizeBytes), "--format", "VDI") // #nosec 17 | 18 | if Verbose { 19 | cmd.Stdout = os.Stdout 20 | cmd.Stderr = os.Stderr 21 | } 22 | 23 | stdin, err := cmd.StdinPipe() 24 | if err != nil { 25 | return err 26 | } 27 | if err = cmd.Start(); err != nil { 28 | return err 29 | } 30 | 31 | n, err := io.Copy(stdin, r) 32 | if err != nil { 33 | return err 34 | } 35 | 36 | // The total number of bytes written to stdin must match sizeBytes, or 37 | // VBoxManage.exe on Windows will fail. Fill remaining with zeros. 38 | if left := sizeBytes - n; left > 0 { 39 | if err := ZeroFill(stdin, left); err != nil { 40 | return err 41 | } 42 | } 43 | 44 | // cmd won't exit until the stdin is closed. 45 | if err := stdin.Close(); err != nil { 46 | return err 47 | } 48 | 49 | return cmd.Wait() 50 | } 51 | 52 | // ZeroFill writes n zero bytes into w. 53 | func ZeroFill(w io.Writer, n int64) error { 54 | const blocksize = 32 << 10 55 | zeros := make([]byte, blocksize) 56 | var k int 57 | var err error 58 | for n > 0 { 59 | if n > blocksize { 60 | k, err = w.Write(zeros) 61 | } else { 62 | k, err = w.Write(zeros[:n]) 63 | } 64 | if err != nil { 65 | return err 66 | } 67 | n -= int64(k) 68 | } 69 | return nil 70 | } 71 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package virtualbox implements wrappers to interact with VirtualBox. 3 | 4 | # VirtualBox Machine State Transition 5 | 6 | A VirtualBox machine can be in one of the following states: 7 | 8 | poweroff: The VM is powered off and no previous running state saved. 9 | running: The VM is running. 10 | paused: The VM is paused, but its state is not saved to disk. If you quit VirtualBox, the state will be lost. 11 | saved: The VM is powered off, and the previous state is saved on disk. 12 | aborted: The VM process crashed. This should happen very rarely. 13 | 14 | VBoxManage supports the following transitions between states: 15 | 16 | startvm : poweroff|saved --> running 17 | controlvm pause: running --> paused 18 | controlvm resume: paused --> running 19 | controlvm savestate: running -> saved 20 | controlvm acpipowerbutton: running --> poweroff 21 | controlvm poweroff: running --> poweroff (unsafe) 22 | controlvm reset: running --> poweroff --> running (unsafe) 23 | 24 | Poweroff and reset are unsafe because they will lose state and might corrupt 25 | the disk image. 26 | 27 | To make things simpler, the following transitions are used instead: 28 | 29 | start: poweroff|saved|paused|aborted --> running 30 | stop: [paused|saved -->] running --> poweroff 31 | save: [paused -->] running --> saved 32 | restart: [paused|saved -->] running --> poweroff --> running 33 | poweroff: [paused|saved -->] running --> poweroff (unsafe) 34 | reset: [paused|saved -->] running --> poweroff --> running (unsafe) 35 | 36 | The takeaway is we try our best to transit the virtual machine into the state 37 | you want it to be, and you only need to watch out for the potentially unsafe 38 | poweroff and reset. 39 | 40 | # Guest Properties Management 41 | 42 | This part of the APIworks both on the host and the guest. 43 | 44 | The API enables to set & get specific properties, plus 45 | waiting on properties patterns (both blocking and non-blocking). 46 | */ 47 | package virtualbox 48 | -------------------------------------------------------------------------------- /example_guestprop_test.go: -------------------------------------------------------------------------------- 1 | package virtualbox_test 2 | 3 | import ( 4 | "log" 5 | "sync" 6 | "time" 7 | 8 | virtualbox "github.com/terra-farm/go-virtualbox" 9 | ) 10 | 11 | var VM = "MyVM" 12 | 13 | func ExampleSetGuestProperty() { 14 | err := virtualbox.SetGuestProperty(VM, "test_name", "test_val") 15 | if err != nil { 16 | panic(err) 17 | } 18 | } 19 | 20 | func ExampleGetGuestProperty() { 21 | err := virtualbox.SetGuestProperty(VM, "test_name", "test_val") 22 | if err != nil { 23 | panic(err) 24 | } 25 | val, err := virtualbox.GetGuestProperty(VM, "test_name") 26 | if err != nil { 27 | panic(err) 28 | } 29 | log.Println("val:", val) 30 | } 31 | 32 | func ExampleDeleteGuestProperty() { 33 | err := virtualbox.SetGuestProperty(VM, "test_name", "test_val") 34 | if err != nil { 35 | panic(err) 36 | } 37 | err = virtualbox.DeleteGuestProperty(VM, "test_name") 38 | if err != nil { 39 | panic(err) 40 | } 41 | } 42 | 43 | func ExampleWaitGuestProperty() { 44 | 45 | go func() { 46 | second := time.Second 47 | time.Sleep(1 * second) 48 | _ = virtualbox.SetGuestProperty(VM, "test_name", "test_val") 49 | }() 50 | 51 | name, val, err := virtualbox.WaitGuestProperty(VM, "test_*") 52 | if err != nil { 53 | panic(err) 54 | } 55 | log.Println("name:", name, ", value:", val) 56 | } 57 | 58 | func ExampleWaitGuestProperties() { 59 | go func() { 60 | second := time.Second 61 | 62 | time.Sleep(1 * second) 63 | _ = virtualbox.SetGuestProperty(VM, "test_name", "test_val1") 64 | 65 | time.Sleep(1 * second) 66 | _ = virtualbox.SetGuestProperty(VM, "test_name", "test_val2") 67 | 68 | time.Sleep(1 * second) 69 | _ = virtualbox.SetGuestProperty(VM, "test_name", "test_val1") 70 | }() 71 | 72 | wg := new(sync.WaitGroup) 73 | done := make(chan bool) 74 | propsPattern := "test_*" 75 | props := virtualbox.WaitGuestProperties(VM, propsPattern, done, wg) 76 | 77 | ok := true 78 | left := 3 79 | for ; ok && left > 0; left-- { 80 | var prop virtualbox.GuestProperty 81 | prop, ok = <-props 82 | log.Println("name:", prop.Name, ", value:", prop.Value) 83 | } 84 | 85 | close(done) // close channel 86 | wg.Wait() // wait for gorouting 87 | } 88 | -------------------------------------------------------------------------------- /extra.go: -------------------------------------------------------------------------------- 1 | package virtualbox 2 | 3 | // SetExtra sets extra data. Name could be "global"|| 4 | func SetExtra(name, key, val string) error { 5 | _, _, err := Manage().run("setextradata", name, key, val) 6 | return err 7 | } 8 | 9 | // DelExtra deletes extra data. Name could be "global"|| 10 | func DelExtra(name, key string) error { 11 | _, _, err := Manage().run("setextradata", name, key) 12 | return err 13 | } 14 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/terra-farm/go-virtualbox 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/go-test/deep v1.0.8 7 | github.com/golang/mock v1.6.0 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= 2 | github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= 3 | github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= 4 | github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= 5 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 6 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 7 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 8 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 9 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 10 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 11 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 12 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 13 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 14 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 15 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 16 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 17 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 18 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 19 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 20 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 21 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 22 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 23 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 24 | golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 25 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 26 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 27 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 28 | -------------------------------------------------------------------------------- /guestprop.go: -------------------------------------------------------------------------------- 1 | package virtualbox 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "regexp" 7 | "strings" 8 | "sync" 9 | ) 10 | 11 | // GuestProperty holds key, value and associated flags. 12 | type GuestProperty struct { 13 | Name string 14 | Value string 15 | } 16 | 17 | var ( 18 | getRegexp = regexp.MustCompile("(?mi)^Value: ([^,]*)$") 19 | waitRegexp = regexp.MustCompile("^Name: ([^,]*), value: ([^,]*), flags:.*$") 20 | ) 21 | 22 | // SetGuestProperty writes a VirtualBox guestproperty to the given value. 23 | func SetGuestProperty(vm string, prop string, val string) error { 24 | if Manage().isGuest() { 25 | _, _, err := Manage().setOpts(sudo(true)).run("guestproperty", "set", prop, val) 26 | return err 27 | } 28 | _, _, err := Manage().run("guestproperty", "set", vm, prop, val) 29 | return err 30 | } 31 | 32 | // GetGuestProperty reads a VirtualBox guestproperty. 33 | func GetGuestProperty(vm string, prop string) (string, error) { 34 | var out string 35 | var err error 36 | if Manage().isGuest() { 37 | out, _, err = Manage().setOpts(sudo(true)).run("guestproperty", "get", prop) 38 | } else { 39 | out, _, err = Manage().run("guestproperty", "get", vm, prop) 40 | } 41 | if err != nil { 42 | return "", err 43 | } 44 | out = strings.TrimSpace(out) 45 | Debug("out (trimmed): '%s'", out) 46 | var match = getRegexp.FindStringSubmatch(out) 47 | Debug("match:", match) 48 | if len(match) != 2 { 49 | return "", fmt.Errorf("No match with get guestproperty output") 50 | } 51 | return match[1], nil 52 | } 53 | 54 | // WaitGuestProperty blocks until a VirtualBox guestproperty is changed 55 | // 56 | // The key to wait for can be a fully defined key or a key wild-card (glob-pattern). 57 | // The first returned value is the property name that was changed. 58 | // The second returned value is the new property value, 59 | // Deletion of the guestproperty causes WaitGuestProperty to return the 60 | // string. 61 | func WaitGuestProperty(vm string, prop string) (string, string, error) { 62 | var out string 63 | var err error 64 | Debug("WaitGuestProperty(): wait on '%s'", prop) 65 | if Manage().isGuest() { 66 | _, _, err = Manage().setOpts(sudo(true)).run("guestproperty", "wait", prop) 67 | if err != nil { 68 | return "", "", err 69 | } 70 | } 71 | out, _, err = Manage().run("guestproperty", "wait", vm, prop) 72 | if err != nil { 73 | log.Print(err) 74 | return "", "", err 75 | } 76 | out = strings.TrimSpace(out) 77 | Debug("WaitGuestProperty(): out (trimmed): '%s'", out) 78 | var match = waitRegexp.FindStringSubmatch(out) 79 | Debug("WaitGuestProperty(): match:", match) 80 | if len(match) != 3 { 81 | return "", "", fmt.Errorf("No match with VBoxManage wait guestproperty output") 82 | } 83 | return match[1], match[2], nil 84 | } 85 | 86 | // WaitGuestProperties wait for changes in GuestProperties 87 | // 88 | // WaitGetProperties wait for changes in the VirtualBox GuestProperties matching 89 | // the given propsPattern, for the given VM. The given bool channel indicates 90 | // caller-required closure. The optional sync.WaitGroup enabke the caller program 91 | // to wait for Go routine completion. 92 | // 93 | // It returns a channel of GuestProperty objects (name-values pairs) populated 94 | // as they change. 95 | // 96 | // If the bool channel is never closed, the Waiter Go routine never ends, 97 | // but on VBoxManage error. 98 | // 99 | // Each GuestProperty change must be read from thwe channel before the waiter Go 100 | // routine resumes waiting for the next matching change. 101 | func WaitGuestProperties(vm string, propPattern string, done chan bool, wg *sync.WaitGroup) chan GuestProperty { 102 | 103 | props := make(chan GuestProperty) 104 | wg.Add(1) 105 | 106 | go func() { 107 | defer close(props) 108 | defer wg.Done() 109 | 110 | for { 111 | Debug("WaitGetProperties(): waiting for: '%s' changes", propPattern) 112 | name, value, err := WaitGuestProperty(vm, propPattern) 113 | if err != nil { 114 | log.Printf("WaitGetProperties(): err=%v", err) 115 | return 116 | } 117 | prop := GuestProperty{name, value} 118 | select { 119 | case props <- prop: 120 | Debug("WaitGetProperties(): stacked: %+v", prop) 121 | case <-done: 122 | Debug("WaitGetProperties(): done channel closed") 123 | return 124 | } 125 | } 126 | }() 127 | 128 | return props 129 | } 130 | 131 | // DeleteGuestProperty deletes a VirtualBox guestproperty. 132 | func DeleteGuestProperty(vm string, prop string) error { 133 | if Manage().isGuest() { 134 | _, _, err := Manage().setOpts(sudo(true)).run("guestproperty", "delete", prop) 135 | return err 136 | } 137 | _, _, err := Manage().run("guestproperty", "delete", vm, prop) 138 | return err 139 | } 140 | -------------------------------------------------------------------------------- /guestprop_test.go: -------------------------------------------------------------------------------- 1 | package virtualbox 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "sync" 7 | "testing" 8 | "time" 9 | 10 | "github.com/golang/mock/gomock" 11 | ) 12 | 13 | func TestGuestProperty(t *testing.T) { 14 | Setup(t) 15 | 16 | t.Logf("ManageMock=%v (type=%T)", ManageMock, ManageMock) 17 | if ManageMock != nil { 18 | ManageMock.EXPECT().isGuest().Return(false) 19 | ManageMock.EXPECT().run("guestproperty", "set", VM, "test_key", "test_val").Return("", "", nil) 20 | } 21 | err := SetGuestProperty(VM, "test_key", "test_val") 22 | if err != nil { 23 | t.Fatal(err) 24 | } 25 | t.Logf("OK SetGuestProperty test_key=test_val") 26 | 27 | if ManageMock != nil { 28 | ManageMock.EXPECT().isGuest().Return(false) 29 | ManageMock.EXPECT().run("guestproperty", "get", VM, "test_key").Return("Value: test_val", "", nil).Times(1) 30 | } 31 | val, err := GetGuestProperty(VM, "test_key") 32 | if err != nil { 33 | t.Fatal(err) 34 | } 35 | t.Logf("val='%s'", val) 36 | if val != "test_val" { 37 | t.Fatal("Wrong value") 38 | } 39 | if ManageMock != nil { 40 | ManageMock.EXPECT().isGuest().Return(false) 41 | ManageMock.EXPECT().run("guestproperty", "get", VM, "test_key").Return("value: test_val", "", nil).Times(1) 42 | } 43 | val_lowercase, err := GetGuestProperty(VM, "test_key") 44 | if err != nil { 45 | t.Fatal(err) 46 | } 47 | t.Logf("val='%s'", val_lowercase) 48 | if val_lowercase != "test_val" { 49 | t.Fatal("Wrong value") 50 | } 51 | Debug("OK GetGuestProperty test_key=test_val") 52 | 53 | // Now deletes it... 54 | if ManageMock != nil { 55 | ManageMock.EXPECT().isGuest().Return(false) 56 | ManageMock.EXPECT().run("guestproperty", "delete", VM, "test_key").Return("", "", nil).Times(1) 57 | } 58 | err = DeleteGuestProperty(VM, "test_key") 59 | if err != nil { 60 | t.Fatal(err) 61 | } 62 | Debug("OK DeleteGuestProperty test_key") 63 | 64 | // ...and check that it is no longer readable 65 | if ManageMock != nil { 66 | ManageMock.EXPECT().isGuest().Return(false) 67 | ManageMock.EXPECT().run("guestproperty", "get", VM, "test_key").Return("", "", errors.New("foo")).Times(1) 68 | } 69 | _, err = GetGuestProperty(VM, "test_key") 70 | if err == nil { 71 | t.Fatal(fmt.Errorf("Failed deleting guestproperty")) 72 | } 73 | Debug("OK GetGuestProperty test_key=empty") 74 | 75 | Teardown() 76 | } 77 | 78 | func TestWaitGuestProperty(t *testing.T) { 79 | Setup(t) 80 | 81 | keyE, valE := "test_key", "test_val1" 82 | if ManageMock != nil { 83 | waitGuestProperty1Out := ReadTestData("vboxmanage-guestproperty-wait-1.out") 84 | gomock.InOrder( 85 | ManageMock.EXPECT().isGuest().Return(false), 86 | ManageMock.EXPECT().run("guestproperty", "wait", VM, "test_*").Return(waitGuestProperty1Out, "", nil).Times(1), 87 | ) 88 | } else { 89 | go func() { 90 | second := time.Second 91 | time.Sleep(1 * second) 92 | t.Logf(">>> key='%s', val='%s'", keyE, valE) 93 | _ = SetGuestProperty(VM, keyE, valE) 94 | }() 95 | } 96 | 97 | keyO, valO, err := WaitGuestProperty(VM, "test_*") 98 | if err != nil { 99 | t.Fatal(err) 100 | } 101 | t.Logf("<<< key='%s', val='%s'", keyO, valO) 102 | if keyE != keyO || valE != valO { 103 | t.Fatal(errors.New("unexpected key/val")) 104 | } 105 | 106 | Teardown() 107 | } 108 | 109 | func TestWaitGuestProperties(t *testing.T) { 110 | Setup(t) 111 | 112 | left := 2 113 | keyE, val1E, val2E := "test_key", "test_val1", "test_val2" 114 | 115 | if ManageMock != nil { 116 | waitGuestProperty1Out := ReadTestData("vboxmanage-guestproperty-wait-1.out") 117 | waitGuestProperty2Out := ReadTestData("vboxmanage-guestproperty-wait-2.out") 118 | gomock.InOrder( 119 | ManageMock.EXPECT().isGuest().Return(false), 120 | ManageMock.EXPECT().run("guestproperty", "wait", VM, "test_*").Return(waitGuestProperty1Out, "", nil).Times(1), 121 | ManageMock.EXPECT().isGuest().Return(false), 122 | ManageMock.EXPECT().run("guestproperty", "wait", VM, "test_*").Return(waitGuestProperty2Out, "", nil).Times(1), 123 | ManageMock.EXPECT().isGuest().Return(false), 124 | ManageMock.EXPECT().run("guestproperty", "wait", VM, "test_*").Return(waitGuestProperty1Out, "", nil).Times(1), 125 | ) 126 | } else { 127 | go func() { 128 | second := time.Second 129 | 130 | time.Sleep(1 * second) 131 | t.Logf(">>> key='%s', val='%s'", keyE, val1E) 132 | _ = SetGuestProperty(VM, keyE, val1E) 133 | 134 | time.Sleep(1 * second) 135 | t.Logf(">>> key='%s', val='%s'", keyE, val2E) 136 | _ = SetGuestProperty(VM, keyE, val2E) 137 | 138 | time.Sleep(1 * second) 139 | t.Logf(">>> key='%s', val='%s'", keyE, val1E) 140 | _ = SetGuestProperty(VM, keyE, val1E) 141 | }() 142 | } 143 | 144 | props := "test_*" 145 | wg := new(sync.WaitGroup) 146 | done := make(chan bool) 147 | 148 | t.Logf("TestWaitGuestProperties(): will wait on '%s' for %d changes\n", props, left) 149 | propsC := WaitGuestProperties(VM, props, done, wg) 150 | 151 | t.Logf("TestWaitGuestProperties(): waiting on: %T(%v)\n", propsC, propsC) 152 | // for prop := range propsChan { 153 | ok := true 154 | for ; ok && left > 0; left-- { 155 | var prop GuestProperty 156 | t.Logf("TestWaitGuestProperties(): unstacking... (left=%d)\n", left) 157 | prop, ok = <-propsC 158 | t.Logf("TestWaitGuestProperties(): unstacked: %+v (left=%d)\n", prop, left) 159 | } 160 | t.Logf("TestWaitGuestProperties(): done...\n") 161 | close(done) 162 | t.Logf("TestWaitGuestProperties(): done... Ok\n") 163 | 164 | wg.Wait() 165 | t.Logf("TestWaitGuestProperties(): exiting\n") 166 | 167 | Teardown() 168 | } 169 | 170 | func TestWaitGuestPropertiesQuit(t *testing.T) { 171 | Setup(t) 172 | 173 | keyE, val1E := "test_key", "test_val1" 174 | 175 | if ManageMock != nil { 176 | waitGuestProperty1Out := ReadTestData("vboxmanage-guestproperty-wait-1.out") 177 | gomock.InOrder( 178 | ManageMock.EXPECT().isGuest().Return(false), 179 | ManageMock.EXPECT().run("guestproperty", "wait", VM, "test_*").Return(waitGuestProperty1Out, "", nil).Times(1), 180 | ) 181 | } else { 182 | go func() { 183 | second := time.Second 184 | 185 | time.Sleep(1 * second) 186 | t.Logf(">>> key='%s', val='%s'", keyE, val1E) 187 | _ = SetGuestProperty(VM, keyE, val1E) 188 | }() 189 | } 190 | 191 | props := "test_*" 192 | wg := new(sync.WaitGroup) 193 | done := make(chan bool) 194 | 195 | t.Logf("TestWaitGuestProperties(): will wait on '%s'\n", props) 196 | propsC := WaitGuestProperties(VM, props, done, wg) 197 | t.Logf("TestWaitGuestProperties(): waiting on: %T(%v)\n", propsC, propsC) 198 | 199 | t.Logf("TestWaitGuestProperties(): done...\n") 200 | close(done) 201 | t.Logf("TestWaitGuestProperties(): done... Ok\n") 202 | 203 | wg.Wait() 204 | t.Logf("TestWaitGuestProperties(): exiting\n") 205 | 206 | Teardown() 207 | } 208 | -------------------------------------------------------------------------------- /hostonlynet.go: -------------------------------------------------------------------------------- 1 | package virtualbox 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "fmt" 7 | "net" 8 | "os/exec" 9 | "regexp" 10 | "runtime" 11 | "strconv" 12 | "strings" 13 | ) 14 | 15 | var ( 16 | reHostonlyInterfaceCreated = regexp.MustCompile(`Interface '(.+)' was successfully created`) 17 | ) 18 | 19 | var ( 20 | // ErrHostonlyInterfaceCreation is the error message created when VBoxManaged failed to create a new hostonly interface. 21 | ErrHostonlyInterfaceCreation = errors.New("failed to create hostonly interface") 22 | ) 23 | 24 | // HostonlyNet defines each host-only network. 25 | type HostonlyNet struct { 26 | Name string 27 | GUID string 28 | DHCP bool 29 | IPv4 net.IPNet 30 | IPv6 net.IPNet 31 | HwAddr net.HardwareAddr 32 | Medium string 33 | Status string 34 | NetworkName string // referenced in DHCP.NetworkName 35 | } 36 | 37 | // CreateHostonlyNet creates a new host-only network. 38 | func CreateHostonlyNet() (*HostonlyNet, error) { 39 | out, _, err := Manage().run("hostonlyif", "create") 40 | if err != nil { 41 | return nil, err 42 | } 43 | res := reHostonlyInterfaceCreated.FindStringSubmatch(out) 44 | if res == nil { 45 | return nil, ErrHostonlyInterfaceCreation 46 | } 47 | return &HostonlyNet{Name: res[1]}, nil 48 | } 49 | 50 | // Config changes the configuration of the host-only network. 51 | func (n *HostonlyNet) Config() error { 52 | 53 | //We need a windowsfix because of https://www.virtualbox.org/ticket/8796 54 | if runtime.GOOS == osWindows { 55 | if n.IPv4.IP != nil && n.IPv4.Mask != nil { 56 | cmd := exec.Command("netsh", "interface", "ip", "set", "address", fmt.Sprintf("name=\"%s\"", n.Name), "static", n.IPv4.IP.String(), net.IP(n.IPv4.Mask).String()) // #nosec 57 | if err := cmd.Run(); err != nil { 58 | return err 59 | } 60 | } 61 | if n.IPv6.IP != nil && n.IPv6.Mask != nil { 62 | prefixLen, _ := n.IPv6.Mask.Size() 63 | cmd := exec.Command("netsh", "interface", "ipv6", "add", "address", n.Name, fmt.Sprintf("%s/%d", n.IPv6.IP.String(), prefixLen)) // #nosec 64 | if err := cmd.Run(); err != nil { 65 | return err 66 | } 67 | } 68 | if n.DHCP { 69 | if _, _, err := Manage().run("hostonlyif", "ipconfig", fmt.Sprintf("\"%s\"", n.Name), "--dhcp"); err != nil { // not implemented as of VirtualBox 4.3 70 | return err 71 | } 72 | } 73 | 74 | } else { 75 | if n.IPv4.IP != nil && n.IPv4.Mask != nil { 76 | if _, _, err := Manage().run("hostonlyif", "ipconfig", n.Name, "--ip", n.IPv4.IP.String(), "--netmask", net.IP(n.IPv4.Mask).String()); err != nil { 77 | return err 78 | } 79 | } 80 | 81 | if n.IPv6.IP != nil && n.IPv6.Mask != nil { 82 | prefixLen, _ := n.IPv6.Mask.Size() 83 | if _, _, err := Manage().run("hostonlyif", "ipconfig", n.Name, "--ipv6", n.IPv6.IP.String(), "--netmasklengthv6", fmt.Sprintf("%d", prefixLen)); err != nil { 84 | return err 85 | } 86 | } 87 | 88 | if n.DHCP { 89 | if _, _, err := Manage().run("hostonlyif", "ipconfig", n.Name, "--dhcp"); err != nil { // not implemented as of VirtualBox 4.3 90 | return err 91 | } 92 | } 93 | } 94 | 95 | return nil 96 | 97 | } 98 | 99 | // HostonlyNets gets all host-only networks in a map keyed by HostonlyNet.NetworkName. 100 | func HostonlyNets() (map[string]*HostonlyNet, error) { 101 | out, _, err := Manage().run("list", "hostonlyifs") 102 | if err != nil { 103 | return nil, err 104 | } 105 | s := bufio.NewScanner(strings.NewReader(out)) 106 | m := map[string]*HostonlyNet{} 107 | n := &HostonlyNet{} 108 | for s.Scan() { 109 | line := s.Text() 110 | if line == "" { 111 | m[n.NetworkName] = n 112 | n = &HostonlyNet{} 113 | continue 114 | } 115 | res := reColonLine.FindStringSubmatch(line) 116 | if res == nil { 117 | continue 118 | } 119 | switch key, val := res[1], res[2]; key { 120 | case "Name": 121 | n.Name = val 122 | case "GUID": 123 | n.GUID = val 124 | case "DHCP": 125 | n.DHCP = (val != "Disabled") 126 | case "IPAddress": 127 | n.IPv4.IP = net.ParseIP(val) 128 | case "NetworkMask": 129 | n.IPv4.Mask = ParseIPv4Mask(val) 130 | case "IPV6Address": 131 | n.IPv6.IP = net.ParseIP(val) 132 | case "IPV6NetworkMaskPrefixLength": 133 | l, err := strconv.ParseUint(val, 10, 7) 134 | if err != nil { 135 | return nil, err 136 | } 137 | n.IPv6.Mask = net.CIDRMask(int(l), net.IPv6len*8) 138 | case "HardwareAddress": 139 | mac, err := net.ParseMAC(val) 140 | if err != nil { 141 | return nil, err 142 | } 143 | n.HwAddr = mac 144 | case "MediumType": 145 | n.Medium = val 146 | case "Status": 147 | n.Status = val 148 | case "VBoxNetworkName": 149 | n.NetworkName = val 150 | } 151 | } 152 | if err := s.Err(); err != nil { 153 | return nil, err 154 | } 155 | return m, nil 156 | } 157 | -------------------------------------------------------------------------------- /hostonlynet_test.go: -------------------------------------------------------------------------------- 1 | package virtualbox 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/golang/mock/gomock" 7 | ) 8 | 9 | func TestHostonlyNets(t *testing.T) { 10 | Setup(t) 11 | 12 | if ManageMock != nil { 13 | listHostOnlyIfsOut := ReadTestData("vboxmanage-list-hostonlyifs-1.out") 14 | gomock.InOrder( 15 | ManageMock.EXPECT().run("list", "hostonlyifs").Return(listHostOnlyIfsOut, "", nil).Times(1), 16 | ) 17 | } 18 | m, err := HostonlyNets() 19 | if err != nil { 20 | t.Fatal(err) 21 | } 22 | for _, n := range m { 23 | t.Logf("%+v", n) 24 | } 25 | 26 | Teardown() 27 | } 28 | -------------------------------------------------------------------------------- /import.go: -------------------------------------------------------------------------------- 1 | package virtualbox 2 | 3 | // ImportOV imports ova or ovf from the given path 4 | func ImportOV(path string) error { 5 | _, _, err := Manage().run("import", path) 6 | return err 7 | } 8 | -------------------------------------------------------------------------------- /init_test.go: -------------------------------------------------------------------------------- 1 | //go:build !go1.13 2 | // +build !go1.13 3 | 4 | package virtualbox 5 | 6 | import ( 7 | "testing" 8 | ) 9 | 10 | func init() { 11 | Debug = LogF 12 | Debug("Using Verbose Log") 13 | Debug("testing.Verbose=%v", testing.Verbose()) 14 | } 15 | -------------------------------------------------------------------------------- /interface.go: -------------------------------------------------------------------------------- 1 | package virtualbox 2 | 3 | import "context" 4 | 5 | // Virtualbox interface defines all the actions which can be performed by the 6 | // Manager. This is mostly a utility interface designed for the customers of the 7 | // package. 8 | type Virtualbox interface { 9 | MachineManager 10 | } 11 | 12 | // MachineManager defines the actions that can be performed to manage machines 13 | type MachineManager interface { 14 | // Machine gets a machine name based on its name or UUID 15 | Machine(context.Context, string) (*Machine, error) 16 | 17 | // ListMachines returns a list of all machines 18 | ListMachines(context.Context) ([]*Machine, error) 19 | 20 | // ModifyMachine allows to update the machine. 21 | ModifyMachine(context.Context, *Machine) error 22 | 23 | // CreateMachine based on the provided information 24 | CreateMachine(context.Context, *Machine) error 25 | 26 | // Start the machine with the given name 27 | StartMachine(context.Context, string) error 28 | 29 | // DeleteMachine deletes a machine by its name or UUID 30 | DeleteMachine(context.Context, string) error 31 | } 32 | -------------------------------------------------------------------------------- /log.go: -------------------------------------------------------------------------------- 1 | package virtualbox 2 | 3 | // LogFunc is the signature to log traces. 4 | type LogFunc func(string, ...interface{}) 5 | 6 | func noLog(string, ...interface{}) {} 7 | 8 | // Debug is the Logger currently in use. 9 | var Debug LogFunc = noLog 10 | -------------------------------------------------------------------------------- /log_test.go: -------------------------------------------------------------------------------- 1 | package virtualbox 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "testing" 8 | ) 9 | 10 | var logger = log.New(os.Stderr, "", 0) 11 | 12 | func logLn(msg string) { 13 | // logger.SetPrefix("\t" + time.Now().Format("2006-01-02 15:04:05") + " ") 14 | logger.SetPrefix("\t ") 15 | logger.Print(msg + "\n") 16 | } 17 | 18 | // LogF 19 | func LogF(format string, args ...interface{}) { 20 | Verbose = testing.Verbose() 21 | if !Verbose { 22 | return 23 | } 24 | logLn(fmt.Sprintf(format, args...)) 25 | } 26 | -------------------------------------------------------------------------------- /machine.go: -------------------------------------------------------------------------------- 1 | package virtualbox 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "errors" 7 | "fmt" 8 | "path/filepath" 9 | "strconv" 10 | "strings" 11 | "time" 12 | ) 13 | 14 | // Machine returns the information about existing virtualbox machine identified 15 | // by either its UUID or name. 16 | func (m *Manager) Machine(ctx context.Context, id string) (*Machine, error) { 17 | m.log.Printf("getting information for %q", id) 18 | // There is a strage behavior where running multiple instances of 19 | // 'VBoxManage showvminfo' on same VM simultaneously can return an error of 20 | // 'object is not ready (E_ACCESSDENIED)', so we sequential the operation with a mutex. 21 | // Note if you are running multiple process of go-virtualbox or 'showvminfo' 22 | // in the command line side by side, this not gonna work. 23 | // TODO: Verify the above is still true. 24 | m.lock.Lock() 25 | stdout, stderr, err := m.run(ctx, "showvminfo", id, "--machinereadable") 26 | m.lock.Unlock() 27 | if err != nil { 28 | if reMachineNotFound.FindString(stderr) != "" { 29 | return nil, ErrMachineNotExist 30 | } 31 | return nil, err 32 | } 33 | 34 | /* Read all VM info into a map */ 35 | props := make(map[string]string) 36 | s := bufio.NewScanner(strings.NewReader(stdout)) 37 | for s.Scan() { 38 | res := reVMInfoLine.FindStringSubmatch(s.Text()) 39 | if res == nil { 40 | continue 41 | } 42 | key := res[1] 43 | if key == "" { 44 | key = res[2] 45 | } 46 | val := res[3] 47 | if val == "" { 48 | val = res[4] 49 | } 50 | props[key] = val 51 | } 52 | 53 | if err := s.Err(); err != nil { 54 | return nil, fmt.Errorf("unable to scan all fields: %w", err) 55 | } 56 | 57 | // error that occured during parsing 58 | var perr error 59 | 60 | sp := func(field string, def ...string) string { 61 | if v, exists := props[field]; exists { 62 | return v 63 | } 64 | if len(def) < 1 { 65 | return "" 66 | } 67 | return def[0] 68 | } 69 | 70 | up := func(field string, def ...uint) uint { 71 | if v, exists := props[field]; exists { 72 | n, err := strconv.ParseUint(v, 10, 32) 73 | if err != nil { 74 | perr = err 75 | return 0 76 | } 77 | return uint(n) 78 | } 79 | if len(def) < 1 { 80 | return 0 81 | } 82 | return def[0] 83 | } 84 | 85 | /* Extract basic info */ 86 | vm := &Machine{ 87 | // TODO: This was in New, verify is this still correct. 88 | BootOrder: make([]string, 0, 4), 89 | NICs: make([]NIC, 0, 4), 90 | Name: sp("name"), 91 | Firmware: sp("firmware"), 92 | UUID: sp("UUID"), 93 | State: MachineState(sp("VMState")), 94 | Memory: up("memory"), 95 | CPUs: up("cpus"), 96 | VRAM: up("vram"), 97 | CfgFile: sp("CfgFile"), 98 | BaseFolder: filepath.Dir(sp("CfgFile")), 99 | } 100 | 101 | /* Extract NIC info */ 102 | for i := 1; i <= 4; i++ { 103 | var nic NIC 104 | nicType, ok := props[fmt.Sprintf("nic%d", i)] 105 | if !ok || nicType == "none" { 106 | break 107 | } 108 | nic.Network = NICNetwork(nicType) 109 | nic.Hardware = NICHardware(props[fmt.Sprintf("nictype%d", i)]) 110 | if nic.Hardware == "" { 111 | return nil, fmt.Errorf("Could not find corresponding 'nictype%d'", i) 112 | } 113 | nic.MacAddr = props[fmt.Sprintf("macaddress%d", i)] 114 | if nic.MacAddr == "" { 115 | return nil, fmt.Errorf("Could not find corresponding 'macaddress%d'", i) 116 | } 117 | if nic.Network == NICNetHostonly { 118 | nic.HostInterface = props[fmt.Sprintf("hostonlyadapter%d", i)] 119 | } else if nic.Network == NICNetBridged { 120 | nic.HostInterface = props[fmt.Sprintf("bridgeadapter%d", i)] 121 | } 122 | vm.NICs = append(vm.NICs, nic) 123 | } 124 | 125 | if perr != nil { 126 | return nil, fmt.Errorf("parsing machine props failed: %w", perr) 127 | } 128 | 129 | return vm, nil 130 | } 131 | 132 | // ListMachines returns the list of the machines 133 | func (m *Manager) ListMachines(ctx context.Context) ([]*Machine, error) { 134 | m.log.Println("listing vms") 135 | stdout, _, err := m.run(ctx, "list", "vms") 136 | if err != nil { 137 | return nil, fmt.Errorf("unable to list vms: %w", err) 138 | } 139 | vms := []*Machine{} 140 | s := bufio.NewScanner(strings.NewReader(stdout)) 141 | for s.Scan() { 142 | res := reVMNameUUID.FindStringSubmatch(s.Text()) 143 | if res == nil { 144 | continue 145 | } 146 | m, err := m.Machine(ctx, res[1]) 147 | if err != nil { 148 | // Sometimes a VM is listed but not available, so we need to handle this. 149 | if errors.Is(err, ErrMachineNotExist) { 150 | continue 151 | } else { 152 | return nil, fmt.Errorf("unable to get machine info: %w", err) 153 | } 154 | } 155 | vms = append(vms, m) 156 | } 157 | if err := s.Err(); err != nil { 158 | return nil, fmt.Errorf("error reading machine list: %w", err) 159 | } 160 | return vms, nil 161 | } 162 | 163 | // ModifyMachine modifies the data of the machine 164 | func (m *Manager) ModifyMachine(ctx context.Context, vm *Machine) error { 165 | args := []string{"modifyvm", vm.Name, 166 | "--firmware", vm.Firmware, 167 | "--bioslogofadein", "off", 168 | "--bioslogofadeout", "off", 169 | "--bioslogodisplaytime", "0", 170 | "--biosbootmenu", "disabled", 171 | 172 | "--ostype", vm.OSType, 173 | "--cpus", fmt.Sprintf("%d", vm.CPUs), 174 | "--memory", fmt.Sprintf("%d", vm.Memory), 175 | "--vram", fmt.Sprintf("%d", vm.VRAM), 176 | 177 | "--acpi", vm.Flag.Get(ACPI), 178 | "--ioapic", vm.Flag.Get(IOAPIC), 179 | "--rtcuseutc", vm.Flag.Get(RTCUSEUTC), 180 | "--cpuhotplug", vm.Flag.Get(CPUHOTPLUG), 181 | "--pae", vm.Flag.Get(PAE), 182 | "--longmode", vm.Flag.Get(LONGMODE), 183 | "--hpet", vm.Flag.Get(HPET), 184 | "--hwvirtex", vm.Flag.Get(HWVIRTEX), 185 | "--triplefaultreset", vm.Flag.Get(TRIPLEFAULTRESET), 186 | "--nestedpaging", vm.Flag.Get(NESTEDPAGING), 187 | "--largepages", vm.Flag.Get(LARGEPAGES), 188 | "--vtxvpid", vm.Flag.Get(VTXVPID), 189 | "--vtxux", vm.Flag.Get(VTXUX), 190 | "--accelerate3d", vm.Flag.Get(ACCELERATE3D), 191 | } 192 | 193 | for i, dev := range vm.BootOrder { 194 | if i > 3 { 195 | break // Only four slots `--boot{1,2,3,4}`. Ignore the rest. 196 | } 197 | args = append(args, fmt.Sprintf("--boot%d", i+1), dev) 198 | } 199 | 200 | for i, nic := range vm.NICs { 201 | n := i + 1 202 | args = append(args, 203 | fmt.Sprintf("--nic%d", n), string(nic.Network), 204 | fmt.Sprintf("--nictype%d", n), string(nic.Hardware), 205 | fmt.Sprintf("--cableconnected%d", n), "on") 206 | if nic.Network == NICNetHostonly { 207 | args = append(args, fmt.Sprintf("--hostonlyadapter%d", n), nic.HostInterface) 208 | } else if nic.Network == NICNetBridged { 209 | args = append(args, fmt.Sprintf("--bridgeadapter%d", n), nic.HostInterface) 210 | } 211 | } 212 | 213 | if _, _, err := m.run(ctx, args...); err != nil { 214 | return err 215 | } 216 | return vm.Refresh() 217 | } 218 | 219 | // StartMachine will start the machine based on its current state. 220 | func (m *Manager) StartMachine(ctx context.Context, id string) error { 221 | var args []string 222 | 223 | vm, err := m.Machine(ctx, id) 224 | if err != nil { 225 | return fmt.Errorf("unable to get machine to check its status: %w", err) 226 | } 227 | 228 | switch vm.State { 229 | case Paused: 230 | args = []string{"controlvm", id, "resume"} 231 | case Poweroff, Saved, Aborted: 232 | args = []string{"startvm", id, "--type", "headless"} 233 | } 234 | 235 | _, msg, err := m.run(ctx, args...) 236 | if err != nil { 237 | return errors.New(msg) 238 | } 239 | 240 | return nil 241 | } 242 | 243 | // MachineState stores the last retrieved VM state. 244 | type MachineState string 245 | 246 | const ( 247 | // Poweroff is a MachineState value. 248 | Poweroff = MachineState("poweroff") 249 | // Running is a MachineState value. 250 | Running = MachineState("running") 251 | // Paused is a MachineState value. 252 | Paused = MachineState("paused") 253 | // Saved is a MachineState value. 254 | Saved = MachineState("saved") 255 | // Aborted is a MachineState value. 256 | Aborted = MachineState("aborted") 257 | ) 258 | 259 | // Flag is an active VM configuration toggle 260 | type Flag int 261 | 262 | // Flag names in lowercases to be consistent with VBoxManage options. 263 | const ( 264 | ACPI Flag = 1 << iota 265 | IOAPIC 266 | RTCUSEUTC 267 | CPUHOTPLUG 268 | PAE 269 | LONGMODE 270 | HPET 271 | HWVIRTEX 272 | TRIPLEFAULTRESET 273 | NESTEDPAGING 274 | LARGEPAGES 275 | VTXVPID 276 | VTXUX 277 | ACCELERATE3D 278 | ) 279 | 280 | // Convert bool to "on"/"off" 281 | func bool2string(b bool) string { 282 | if b { 283 | return "on" 284 | } 285 | return "off" 286 | } 287 | 288 | // Get tests if flag is set. Return "on" or "off". 289 | func (f Flag) Get(o Flag) string { 290 | return bool2string(f&o == o) 291 | } 292 | 293 | // Machine information. 294 | type Machine struct { 295 | Name string 296 | Firmware string 297 | UUID string 298 | State MachineState 299 | CPUs uint 300 | Memory uint // main memory (in MB) 301 | VRAM uint // video memory (in MB) 302 | CfgFile string 303 | BaseFolder string 304 | OSType string 305 | Flag Flag 306 | BootOrder []string // max 4 slots, each in {none|floppy|dvd|disk|net} 307 | NICs []NIC 308 | } 309 | 310 | // New creates a new machine. 311 | func New() *Machine { 312 | return &Machine{ 313 | BootOrder: make([]string, 0, 4), 314 | NICs: make([]NIC, 0, 4), 315 | } 316 | } 317 | 318 | // Refresh reloads the machine information. 319 | func (m *Machine) Refresh() error { 320 | id := m.Name 321 | if id == "" { 322 | id = m.UUID 323 | } 324 | mm, err := GetMachine(id) 325 | if err != nil { 326 | return err 327 | } 328 | *m = *mm 329 | return nil 330 | } 331 | 332 | // Start the machine, and return the underlying error when unable to do so. 333 | func (m *Machine) Start() error { 334 | return defaultManager.StartMachine(context.Background(), m.UUID) 335 | } 336 | 337 | // DisconnectSerialPort sets given serial port to disconnected. 338 | func (m *Machine) DisconnectSerialPort(portNumber int) error { 339 | _, _, err := Manage().run("modifyvm", m.Name, fmt.Sprintf("--uartmode%d", portNumber), "disconnected") 340 | return err 341 | } 342 | 343 | // Save suspends the machine and saves its state to disk. 344 | func (m *Machine) Save() error { 345 | switch m.State { 346 | case Paused: 347 | if err := m.Start(); err != nil { 348 | return err 349 | } 350 | case Poweroff, Aborted, Saved: 351 | return nil 352 | } 353 | _, _, err := Manage().run("controlvm", m.Name, "savestate") 354 | return err 355 | } 356 | 357 | // Pause pauses the execution of the machine. 358 | func (m *Machine) Pause() error { 359 | switch m.State { 360 | case Paused, Poweroff, Aborted, Saved: 361 | return nil 362 | } 363 | _, _, err := Manage().run("controlvm", m.Name, "pause") 364 | return err 365 | } 366 | 367 | // Stop gracefully stops the machine. 368 | func (m *Machine) Stop() error { 369 | switch m.State { 370 | case Poweroff, Aborted, Saved: 371 | return nil 372 | case Paused: 373 | if err := m.Start(); err != nil { 374 | return err 375 | } 376 | } 377 | 378 | for m.State != Poweroff { // busy wait until the machine is stopped 379 | if _, _, err := Manage().run("controlvm", m.Name, "acpipowerbutton"); err != nil { 380 | return err 381 | } 382 | time.Sleep(1 * time.Second) 383 | if err := m.Refresh(); err != nil { 384 | return err 385 | } 386 | } 387 | return nil 388 | } 389 | 390 | // Poweroff forcefully stops the machine. State is lost and might corrupt the disk image. 391 | func (m *Machine) Poweroff() error { 392 | switch m.State { 393 | case Poweroff, Aborted, Saved: 394 | return nil 395 | } 396 | _, _, err := Manage().run("controlvm", m.Name, "poweroff") 397 | return err 398 | } 399 | 400 | // Restart gracefully restarts the machine. 401 | func (m *Machine) Restart() error { 402 | switch m.State { 403 | case Paused, Saved: 404 | if err := m.Start(); err != nil { 405 | return err 406 | } 407 | } 408 | if err := m.Stop(); err != nil { 409 | return err 410 | } 411 | return m.Start() 412 | } 413 | 414 | // Reset forcefully restarts the machine. State is lost and might corrupt the disk image. 415 | func (m *Machine) Reset() error { 416 | switch m.State { 417 | case Paused, Saved: 418 | if err := m.Start(); err != nil { 419 | return err 420 | } 421 | } 422 | _, _, err := Manage().run("controlvm", m.Name, "reset") 423 | return err 424 | } 425 | 426 | // Delete deletes the machine and associated disk images. 427 | func (m *Machine) Delete() error { 428 | if err := m.Poweroff(); err != nil { 429 | return err 430 | } 431 | _, _, err := Manage().run("unregistervm", m.Name, "--delete") 432 | return err 433 | } 434 | 435 | // GetMachine finds a machine by its name or UUID. 436 | // DEPRECATED: Use (*Manager).Machine 437 | func GetMachine(id string) (*Machine, error) { 438 | return defaultManager.Machine(context.Background(), id) 439 | } 440 | 441 | // ListMachines lists all registered machines. 442 | // DEPRECATED: Use (*Manager).ListMachines 443 | func ListMachines() ([]*Machine, error) { 444 | return defaultManager.ListMachines(context.Background()) 445 | } 446 | 447 | // CreateMachine creates a new machine. If basefolder is empty, use default. 448 | func CreateMachine(name, basefolder string) (*Machine, error) { 449 | if name == "" { 450 | return nil, fmt.Errorf("machine name is empty") 451 | } 452 | 453 | // Check if a machine with the given name already exists. 454 | ms, err := ListMachines() 455 | if err != nil { 456 | return nil, err 457 | } 458 | for _, m := range ms { 459 | if m.Name == name { 460 | return nil, ErrMachineExist 461 | } 462 | } 463 | 464 | // Create and register the machine. 465 | args := []string{"createvm", "--name", name, "--register"} 466 | if basefolder != "" { 467 | args = append(args, "--basefolder", basefolder) 468 | } 469 | if _, _, err = Manage().run(args...); err != nil { 470 | return nil, err 471 | } 472 | 473 | m, err := GetMachine(name) 474 | if err != nil { 475 | return nil, err 476 | } 477 | 478 | return m, nil 479 | } 480 | 481 | // Modify changes the settings of the machine. 482 | // DEPRECATED: Use (*Manager).ModifyMachine 483 | func (m *Machine) Modify() error { 484 | return defaultManager.ModifyMachine(context.Background(), m) 485 | } 486 | 487 | // AddNATPF adds a NAT port forarding rule to the n-th NIC with the given name. 488 | func (m *Machine) AddNATPF(n int, name string, rule PFRule) error { 489 | _, _, err := Manage().run("controlvm", m.Name, fmt.Sprintf("natpf%d", n), 490 | fmt.Sprintf("%s,%s", name, rule.Format())) 491 | return err 492 | } 493 | 494 | // DelNATPF deletes the NAT port forwarding rule with the given name from the n-th NIC. 495 | func (m *Machine) DelNATPF(n int, name string) error { 496 | _, _, err := Manage().run("controlvm", m.Name, fmt.Sprintf("natpf%d", n), "delete", name) 497 | return err 498 | } 499 | 500 | // SetNIC set the n-th NIC. 501 | func (m *Machine) SetNIC(n int, nic NIC) error { 502 | args := []string{"modifyvm", m.Name, 503 | fmt.Sprintf("--nic%d", n), string(nic.Network), 504 | fmt.Sprintf("--nictype%d", n), string(nic.Hardware), 505 | fmt.Sprintf("--cableconnected%d", n), "on", 506 | } 507 | 508 | if nic.Network == NICNetHostonly { 509 | args = append(args, fmt.Sprintf("--hostonlyadapter%d", n), nic.HostInterface) 510 | } else if nic.Network == NICNetBridged { 511 | args = append(args, fmt.Sprintf("--bridgeadapter%d", n), nic.HostInterface) 512 | } 513 | _, _, err := Manage().run(args...) 514 | return err 515 | } 516 | 517 | // AddStorageCtl adds a storage controller with the given name. 518 | func (m *Machine) AddStorageCtl(name string, ctl StorageController) error { 519 | args := []string{"storagectl", m.Name, "--name", name} 520 | if ctl.SysBus != "" { 521 | args = append(args, "--add", string(ctl.SysBus)) 522 | } 523 | if ctl.Ports > 0 { 524 | args = append(args, "--portcount", fmt.Sprintf("%d", ctl.Ports)) 525 | } 526 | if ctl.Chipset != "" { 527 | args = append(args, "--controller", string(ctl.Chipset)) 528 | } 529 | args = append(args, "--hostiocache", bool2string(ctl.HostIOCache)) 530 | args = append(args, "--bootable", bool2string(ctl.Bootable)) 531 | 532 | _, _, err := Manage().run(args...) 533 | return err 534 | } 535 | 536 | // DelStorageCtl deletes the storage controller with the given name. 537 | func (m *Machine) DelStorageCtl(name string) error { 538 | _, _, err := Manage().run("storagectl", m.Name, "--name", name, "--remove") 539 | return err 540 | } 541 | 542 | // AttachStorage attaches a storage medium to the named storage controller. 543 | func (m *Machine) AttachStorage(ctlName string, medium StorageMedium) error { 544 | _, _, err := Manage().run("storageattach", m.Name, "--storagectl", ctlName, 545 | "--port", fmt.Sprintf("%d", medium.Port), 546 | "--device", fmt.Sprintf("%d", medium.Device), 547 | "--type", string(medium.DriveType), 548 | "--medium", medium.Medium, 549 | ) 550 | return err 551 | } 552 | 553 | // SetExtraData attaches custom string to the VM. 554 | func (m *Machine) SetExtraData(key, val string) error { 555 | _, _, err := Manage().run("setextradata", m.Name, key, val) 556 | return err 557 | } 558 | 559 | // GetExtraData retrieves custom string from the VM. 560 | func (m *Machine) GetExtraData(key string) (*string, error) { 561 | value, _, err := Manage().run("getextradata", m.Name, key) 562 | if err != nil { 563 | return nil, err 564 | } 565 | value = strings.TrimSpace(value) 566 | /* 'getextradata get' returns 0 even when the key is not found, 567 | so we need to check stdout for this case */ 568 | if strings.HasPrefix(value, "No value set") { 569 | return nil, nil 570 | } 571 | trimmed := strings.TrimPrefix(value, "Value: ") 572 | return &trimmed, nil 573 | } 574 | 575 | // DeleteExtraData removes custom string from the VM. 576 | func (m *Machine) DeleteExtraData(key string) error { 577 | _, _, err := Manage().run("setextradata", m.Name, key) 578 | return err 579 | } 580 | 581 | // CloneMachine clones the given machine name into a new one. 582 | func CloneMachine(baseImageName string, newImageName string, register bool) error { 583 | if register { 584 | _, _, err := Manage().run("clonevm", baseImageName, "--name", newImageName, "--register") 585 | return err 586 | } 587 | _, _, err := Manage().run("clonevm", baseImageName, "--name", newImageName) 588 | return err 589 | } 590 | -------------------------------------------------------------------------------- /machine_test.go: -------------------------------------------------------------------------------- 1 | package virtualbox 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "testing" 7 | 8 | "github.com/go-test/deep" 9 | ) 10 | 11 | var ( 12 | testUbuntuMachine = &Machine{ 13 | Name: "Ubuntu", 14 | Firmware: "BIOS", 15 | UUID: "37f5d336-bf07-48dd-947c-37e6a56420a7", 16 | State: Saved, 17 | CPUs: 1, 18 | Memory: 1024, VRAM: 8, CfgFile: "/Users/fix/VirtualBox VMs/go-virtualbox/go-virtualbox.vbox", 19 | BaseFolder: "/Users/fix/VirtualBox VMs/go-virtualbox", OSType: "", Flag: 0, BootOrder: []string{}, 20 | NICs: []NIC{ 21 | {Network: "nat", Hardware: "82540EM", HostInterface: "", MacAddr: "080027EE1DF7"}, 22 | }, 23 | } 24 | testGoVirtualboxMachine = &Machine{ 25 | Name: "go-virtualbox", 26 | Firmware: "BIOS", 27 | UUID: "37f5d336-bf08-48dd-947c-37e6a56420a7", 28 | State: Saved, 29 | CPUs: 1, 30 | Memory: 1024, VRAM: 8, CfgFile: "/Users/fix/VirtualBox VMs/go-virtualbox/go-virtualbox.vbox", 31 | BaseFolder: "/Users/fix/VirtualBox VMs/go-virtualbox", OSType: "", Flag: 0, BootOrder: []string{}, 32 | NICs: []NIC{ 33 | {Network: "nat", Hardware: "82540EM", HostInterface: "", MacAddr: "080027EE1DF7"}, 34 | }, 35 | } 36 | ) 37 | 38 | func TestMachine(t *testing.T) { 39 | testCases := map[string]struct { 40 | in string 41 | want *Machine 42 | err error 43 | }{ 44 | "by name": { 45 | in: "Ubuntu", 46 | want: testUbuntuMachine, 47 | err: nil, 48 | }, 49 | } 50 | for name, tc := range testCases { 51 | t.Run(name, func(t *testing.T) { 52 | m := newTestManager() 53 | 54 | got, err := m.Machine(context.Background(), tc.in) 55 | if diff := deep.Equal(got, tc.want); !errors.Is(err, tc.err) || diff != nil { 56 | t.Errorf("Machine(%s) = %+v, %v; want %v, %v; diff = %v", 57 | tc.in, got, err, tc.want, tc.err, diff) 58 | } 59 | }) 60 | } 61 | } 62 | 63 | func TestListMachines(t *testing.T) { 64 | testCases := map[string]struct { 65 | want []*Machine 66 | err error 67 | }{ 68 | "good": { 69 | // TODO: If this relies on order we should ensure that it will be 70 | // consistent for the tests. 71 | want: []*Machine{testUbuntuMachine, testGoVirtualboxMachine}, 72 | err: nil, 73 | }, 74 | } 75 | 76 | for name, tc := range testCases { 77 | t.Run(name, func(t *testing.T) { 78 | m := newTestManager() 79 | 80 | got, err := m.ListMachines(context.Background()) 81 | if diff := deep.Equal(got, tc.want); !errors.Is(err, tc.err) || diff != nil { 82 | t.Errorf("ListMachines() = %v, %v; want %v, %v; diff = %v", 83 | got, err, tc.want, tc.err, diff) 84 | } 85 | }) 86 | } 87 | } 88 | 89 | func TestModifyMachine(t *testing.T) { 90 | // TODO: Figure out how we can do this test, it has pretty extensive flag list 91 | // so having a file in the testdata with such a long name doesn't make 92 | // sense. 93 | } 94 | -------------------------------------------------------------------------------- /manager.go: -------------------------------------------------------------------------------- 1 | package virtualbox 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "log" 7 | "os" 8 | "sync" 9 | ) 10 | 11 | // runFn is the function which is used to actually run the commands. This is 12 | // abstracted into a function so it can be easily replaced for testing purposes. 13 | type runFn func(context.Context, ...string) (string, string, error) 14 | 15 | // Manager of the virtualbox instance. 16 | type Manager struct { 17 | // lock the whole manager to only allow one action at a time 18 | // TODO: Decide if this is a good idea, maybe one mutex per type of operation? 19 | lock sync.Mutex 20 | 21 | run runFn 22 | 23 | log *log.Logger 24 | } 25 | 26 | // NewManager returns a manager capable of managing everything in virtualbox. 27 | func NewManager(opts ...Option) *Manager { 28 | m := &Manager{ 29 | run: vboxManageRun, 30 | log: log.New(io.Discard, "", 0), 31 | } 32 | 33 | // if the debug env var for the virtualbox is set to true, we want to set the 34 | // logger to be bit more useful, and the default logger will suffice. 35 | if os.Getenv("DEBUG") == "virtualbox" { 36 | m.log = log.Default() 37 | } 38 | 39 | for _, opt := range opts { 40 | opt(m) 41 | } 42 | 43 | return m 44 | } 45 | 46 | // vboxManageRun is a function which actually runs the VboxManage 47 | func vboxManageRun(_ context.Context, args ...string) (string, string, error) { 48 | // TODO: reimplement and do not use the old function 49 | return Manage().run(args...) 50 | } 51 | 52 | // defaultManager is used for backwards compatibility so that the older 53 | // functions can use it. 54 | var defaultManager = NewManager() 55 | 56 | // Option modifies the manager options 57 | type Option func(*Manager) 58 | 59 | // Logger allows to override the logger used by the manager. 60 | func Logger(l *log.Logger) Option { 61 | return func(m *Manager) { 62 | m.log = l 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /manager_test.go: -------------------------------------------------------------------------------- 1 | package virtualbox 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | ) 11 | 12 | func newTestManager() *Manager { 13 | m := NewManager(Logger(log.Default())) 14 | m.run = testDataRun 15 | 16 | return m 17 | } 18 | 19 | // testDataRun returns the test data for the given args 20 | func testDataRun(_ context.Context, args ...string) (string, string, error) { 21 | // joined args create the file information 22 | name := filepath.Join("testdata", strings.Join(args, "_")+".out") 23 | data, err := os.ReadFile(name) 24 | if err != nil { 25 | return "", "", fmt.Errorf("unable to open testdata file %s: %v", name, err) 26 | } 27 | return string(data), "", nil 28 | } 29 | -------------------------------------------------------------------------------- /natnet.go: -------------------------------------------------------------------------------- 1 | package virtualbox 2 | 3 | import ( 4 | "bufio" 5 | "net" 6 | "strings" 7 | ) 8 | 9 | // A NATNet defines a NAT network. 10 | type NATNet struct { 11 | Name string 12 | IPv4 net.IPNet 13 | IPv6 net.IPNet 14 | DHCP bool 15 | Enabled bool 16 | } 17 | 18 | // NATNets gets all NAT networks in a map keyed by NATNet.Name. 19 | func NATNets() (map[string]NATNet, error) { 20 | out, _, err := Manage().run("list", "natnets") 21 | if err != nil { 22 | return nil, err 23 | } 24 | s := bufio.NewScanner(strings.NewReader(out)) 25 | m := map[string]NATNet{} 26 | n := NATNet{} 27 | for s.Scan() { 28 | line := s.Text() 29 | if line == "" { 30 | m[n.Name] = n 31 | n = NATNet{} 32 | continue 33 | } 34 | res := reColonLine.FindStringSubmatch(line) 35 | if res == nil { 36 | continue 37 | } 38 | switch key, val := res[1], res[2]; key { 39 | case "NetworkName": 40 | n.Name = val 41 | case "IP": 42 | n.IPv4.IP = net.ParseIP(val) 43 | case "Network": 44 | _, ipnet, err := net.ParseCIDR(val) 45 | if err != nil { 46 | return nil, err 47 | } 48 | n.IPv4.Mask = ipnet.Mask 49 | case "IPv6 Prefix": 50 | // TODO: IPv6 CIDR parsing works fine on macOS, check on Windows 51 | // if val == "" { 52 | // continue 53 | // } 54 | // l, err := strconv.ParseUint(val, 10, 7) 55 | // if err != nil { 56 | // return nil, err 57 | // } 58 | // n.IPv6.Mask = net.CIDRMask(int(l), net.IPv6len*8) 59 | _, ipnet, err := net.ParseCIDR(val) 60 | if err != nil { 61 | return nil, err 62 | } 63 | n.IPv6.Mask = ipnet.Mask 64 | case "DHCP Enabled": 65 | n.DHCP = (val == stringYes) 66 | case "Enabled": 67 | n.Enabled = (val == stringYes) 68 | } 69 | } 70 | if err := s.Err(); err != nil { 71 | return nil, err 72 | } 73 | return m, nil 74 | } 75 | -------------------------------------------------------------------------------- /natnet_test.go: -------------------------------------------------------------------------------- 1 | package virtualbox 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/golang/mock/gomock" 7 | ) 8 | 9 | func TestNATNets(t *testing.T) { 10 | Setup(t) 11 | 12 | if ManageMock != nil { 13 | listHostOnlyIfsOut := ReadTestData("vboxmanage-list-natnets-1.out") 14 | gomock.InOrder( 15 | ManageMock.EXPECT().run("list", "natnets").Return(listHostOnlyIfsOut, "", nil).Times(1), 16 | ) 17 | } 18 | m, err := NATNets() 19 | if err != nil { 20 | t.Fatal(err) 21 | } 22 | t.Logf("%+v", m) 23 | 24 | Teardown() 25 | } 26 | -------------------------------------------------------------------------------- /nic.go: -------------------------------------------------------------------------------- 1 | package virtualbox 2 | 3 | // NIC represents a virtualized network interface card. 4 | type NIC struct { 5 | Network NICNetwork 6 | Hardware NICHardware 7 | HostInterface string // The host interface name to bind to in 'hostonly' and 'bridged' mode 8 | MacAddr string 9 | } 10 | 11 | // NICNetwork represents the type of NIC networks. 12 | type NICNetwork string 13 | 14 | const ( 15 | // NICNetAbsent when there is no NIC. 16 | NICNetAbsent = NICNetwork("none") 17 | // NICNetDisconnected when the NIC is disconnected 18 | NICNetDisconnected = NICNetwork("null") 19 | // NICNetNAT when the NIC is NAT-ed to access the external network. 20 | NICNetNAT = NICNetwork("nat") 21 | // NICNetBridged when the NIC is the bridge to the external network. 22 | NICNetBridged = NICNetwork("bridged") 23 | // NICNetInternal when the NIC does not have access to the external network. 24 | NICNetInternal = NICNetwork("intnet") 25 | // NICNetHostonly when the NIC can only access one host-only network. 26 | NICNetHostonly = NICNetwork("hostonly") 27 | // NICNetGeneric when the NIC behaves like a standard physical one. 28 | NICNetGeneric = NICNetwork("generic") 29 | ) 30 | 31 | // NICHardware represents the type of NIC hardware. 32 | type NICHardware string 33 | 34 | const ( 35 | // AMDPCNetPCIII when the NIC emulates a Am79C970A hardware. 36 | AMDPCNetPCIII = NICHardware("Am79C970A") 37 | // AMDPCNetFASTIII when the NIC emulates a Am79C973 hardware. 38 | AMDPCNetFASTIII = NICHardware("Am79C973") 39 | // IntelPro1000MTDesktop when the NIC emulates a 82540EM hardware. 40 | IntelPro1000MTDesktop = NICHardware("82540EM") 41 | // IntelPro1000TServer when the NIC emulates a 82543GC hardware. 42 | IntelPro1000TServer = NICHardware("82543GC") 43 | // IntelPro1000MTServer when the NIC emulates a 82545EM hardware. 44 | IntelPro1000MTServer = NICHardware("82545EM") 45 | // VirtIO when the NIC emulates a virtio. 46 | VirtIO = NICHardware("virtio") 47 | ) 48 | -------------------------------------------------------------------------------- /pfrule.go: -------------------------------------------------------------------------------- 1 | package virtualbox 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | ) 7 | 8 | // PFRule represents a port forwarding rule. 9 | type PFRule struct { 10 | Proto PFProto 11 | HostIP net.IP // can be nil to match any host interface 12 | GuestIP net.IP // can be nil if guest IP is leased from built-in DHCP 13 | HostPort uint16 14 | GuestPort uint16 15 | } 16 | 17 | // PFProto represents the protocol of a port forwarding rule. 18 | type PFProto string 19 | 20 | const ( 21 | // PFTCP when forwarding a TCP port. 22 | PFTCP = PFProto("tcp") 23 | // PFUDP when forwarding an UDP port. 24 | PFUDP = PFProto("udp") 25 | ) 26 | 27 | // String returns a human-friendly representation of the port forwarding rule. 28 | func (r PFRule) String() string { 29 | hostip, guestip := grab(r) 30 | return fmt.Sprintf("%s://%s:%d --> %s:%d", 31 | r.Proto, hostip, r.HostPort, 32 | guestip, r.GuestPort) 33 | } 34 | 35 | // Format returns the string needed as a command-line argument to VBoxManage. 36 | func (r PFRule) Format() string { 37 | hostip, guestip := grab(r) 38 | return fmt.Sprintf("%s,%s,%d,%s,%d", r.Proto, hostip, r.HostPort, guestip, r.GuestPort) 39 | } 40 | 41 | func grab(r PFRule) (string, string) { 42 | hostip := "" 43 | if r.HostIP != nil { 44 | hostip = r.HostIP.String() 45 | } 46 | guestip := "" 47 | if r.GuestIP != nil { 48 | guestip = r.GuestIP.String() 49 | } 50 | return hostip, guestip 51 | } 52 | -------------------------------------------------------------------------------- /regexp_test.go: -------------------------------------------------------------------------------- 1 | package virtualbox 2 | 3 | import ( 4 | "os" 5 | "regexp" 6 | "testing" 7 | ) 8 | 9 | func TestGetRegexp(t *testing.T) { 10 | var str = os.Getenv("TEST_STRING") 11 | if len(str) <= 0 { 12 | str = "Value: foo" 13 | } 14 | var re *regexp.Regexp 15 | var reStr = os.Getenv("TEST_GETREGEXP") 16 | if len(reStr) <= 0 { 17 | re = getRegexp 18 | } else { 19 | re = regexp.MustCompile(reStr) 20 | } 21 | var match = re.FindStringSubmatch(str) 22 | t.Log("match:", match) 23 | if len(match) != 2 { 24 | t.Fatal("No match") 25 | } 26 | if match[0] != str { 27 | t.Fatal("No global match") 28 | } 29 | if match[1] != "foo" { 30 | t.Fatal("No value match") 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /storage.go: -------------------------------------------------------------------------------- 1 | package virtualbox 2 | 3 | // StorageController represents a virtualized storage controller. 4 | type StorageController struct { 5 | SysBus SystemBus 6 | Ports uint // SATA port count 1--30 7 | Chipset StorageControllerChipset 8 | HostIOCache bool 9 | Bootable bool 10 | } 11 | 12 | // SystemBus represents the system bus of a storage controller. 13 | type SystemBus string 14 | 15 | const ( 16 | // SysBusIDE when the storage controller provides an IDE bus. 17 | SysBusIDE = SystemBus("ide") 18 | // SysBusSATA when the storage controller provides a SATA bus. 19 | SysBusSATA = SystemBus("sata") 20 | // SysBusSCSI when the storage controller provides an SCSI bus. 21 | SysBusSCSI = SystemBus("scsi") 22 | // SysBusFloppy when the storage controller provides access to Floppy drives. 23 | SysBusFloppy = SystemBus("floppy") 24 | // SysBusSAS storage controller provides a SAS bus. 25 | SysBusSAS = SystemBus("sas") 26 | // SysBusUSB storage controller proveds an USB bus. 27 | SysBusUSB = SystemBus("usb") 28 | // SysBusPCIE storage controller proveds a PCIe bus. 29 | SysBusPCIE = SystemBus("pcie") 30 | // SysBusVirtio storage controller proveds a Virtio bus. 31 | SysBusVirtio = SystemBus("virtio") 32 | ) 33 | 34 | // StorageControllerChipset represents the hardware of a storage controller. 35 | type StorageControllerChipset string 36 | 37 | const ( 38 | // CtrlLSILogic when the storage controller emulates LSILogic hardware. 39 | CtrlLSILogic = StorageControllerChipset("LSILogic") 40 | // CtrlLSILogicSAS when the storage controller emulates LSILogicSAS hardware. 41 | CtrlLSILogicSAS = StorageControllerChipset("LSILogicSAS") 42 | // CtrlBusLogic when the storage controller emulates BusLogic hardware. 43 | CtrlBusLogic = StorageControllerChipset("BusLogic") 44 | // CtrlIntelAHCI when the storage controller emulates IntelAHCI hardware. 45 | CtrlIntelAHCI = StorageControllerChipset("IntelAHCI") 46 | // CtrlPIIX3 when the storage controller emulates PIIX3 hardware. 47 | CtrlPIIX3 = StorageControllerChipset("PIIX3") 48 | // CtrlPIIX4 when the storage controller emulates PIIX4 hardware. 49 | CtrlPIIX4 = StorageControllerChipset("PIIX4") 50 | // CtrlICH6 when the storage controller emulates ICH6 hardware. 51 | CtrlICH6 = StorageControllerChipset("ICH6") 52 | // CtrlI82078 when the storage controller emulates I82078 hardware. 53 | CtrlI82078 = StorageControllerChipset("I82078") 54 | // CtrlUSB storage controller emulates USB hardware. 55 | CtrlUSB = StorageControllerChipset("USB") 56 | // CtrlNVME storage controller emulates NVME hardware. 57 | CtrlNVME = StorageControllerChipset("NVMe") 58 | // CtrlVirtIO storage controller emulates VirtIO hardware. 59 | CtrlVirtIO = StorageControllerChipset("VirtIO") 60 | ) 61 | 62 | // StorageMedium represents the storage medium attached to a storage controller. 63 | type StorageMedium struct { 64 | Port uint 65 | Device uint 66 | DriveType DriveType 67 | Medium string // none|emptydrive|||iscsi 68 | } 69 | 70 | // DriveType represents the hardware type of a drive. 71 | type DriveType string 72 | 73 | const ( 74 | // DriveDVD when the drive is a DVD reader/writer. 75 | DriveDVD = DriveType("dvddrive") 76 | // DriveHDD when the drive is a hard disk or SSD. 77 | DriveHDD = DriveType("hdd") 78 | // DriveFDD when the drive is a floppy. 79 | DriveFDD = DriveType("fdd") 80 | ) 81 | 82 | // CloneHD virtual harddrive 83 | func CloneHD(input, output string) error { 84 | _, _, err := Manage().run("clonehd", input, output) 85 | return err 86 | } 87 | -------------------------------------------------------------------------------- /testdata/list_vms.out: -------------------------------------------------------------------------------- 1 | "Ubuntu" {2e16b1fc-675d-4a7a-a9a1-e89a8bde7874} 2 | "go-virtualbox" {def44546-e3da-4902-8d15-b91c99c80cbc} 3 | -------------------------------------------------------------------------------- /testdata/showvminfo_Ubuntu_--machinereadable.out: -------------------------------------------------------------------------------- 1 | name="Ubuntu" 2 | groups="/" 3 | ostype="Ubuntu (64-bit)" 4 | UUID="37f5d336-bf07-48dd-947c-37e6a56420a7" 5 | CfgFile="/Users/fix/VirtualBox VMs/go-virtualbox/go-virtualbox.vbox" 6 | SnapFldr="/Users/fix/VirtualBox VMs/go-virtualbox/Snapshots" 7 | LogFldr="/Users/fix/VirtualBox VMs/go-virtualbox/Logs" 8 | hardwareuuid="37f5d336-bf07-48dd-947c-37e6a56420a7" 9 | memory=1024 10 | pagefusion="off" 11 | vram=8 12 | cpuexecutioncap=100 13 | hpet="off" 14 | chipset="piix3" 15 | firmware="BIOS" 16 | cpus=1 17 | pae="on" 18 | longmode="on" 19 | triplefaultreset="off" 20 | apic="on" 21 | x2apic="on" 22 | cpuid-portability-level=0 23 | bootmenu="messageandmenu" 24 | boot1="disk" 25 | boot2="dvd" 26 | boot3="none" 27 | boot4="none" 28 | acpi="on" 29 | ioapic="on" 30 | biosapic="apic" 31 | biossystemtimeoffset=0 32 | rtcuseutc="on" 33 | hwvirtex="on" 34 | nestedpaging="on" 35 | largepages="on" 36 | vtxvpid="on" 37 | vtxux="on" 38 | paravirtprovider="default" 39 | effparavirtprovider="kvm" 40 | VMState="saved" 41 | VMStateChangeTime="2018-04-23T09:29:53.476000000" 42 | VMStateFile="/Users/fix/VirtualBox VMs/go-virtualbox/Snapshots/2018-04-23T09-29-48-014952000Z.sav" 43 | monitorcount=1 44 | accelerate3d="off" 45 | accelerate2dvideo="off" 46 | teleporterenabled="off" 47 | teleporterport=0 48 | teleporteraddress="" 49 | teleporterpassword="" 50 | tracing-enabled="off" 51 | tracing-allow-vm-access="off" 52 | tracing-config="" 53 | autostart-enabled="off" 54 | autostart-delay=0 55 | defaultfrontend="" 56 | storagecontrollername0="IDE Controller" 57 | storagecontrollertype0="PIIX4" 58 | storagecontrollerinstance0="0" 59 | storagecontrollermaxportcount0="2" 60 | storagecontrollerportcount0="2" 61 | storagecontrollerbootable0="on" 62 | storagecontrollername1="SATA Controller" 63 | storagecontrollertype1="IntelAhci" 64 | storagecontrollerinstance1="0" 65 | storagecontrollermaxportcount1="30" 66 | storagecontrollerportcount1="1" 67 | storagecontrollerbootable1="on" 68 | "IDE Controller-0-0"="none" 69 | "IDE Controller-0-1"="none" 70 | "IDE Controller-1-0"="none" 71 | "IDE Controller-1-1"="none" 72 | "SATA Controller-0-0"="/Users/fix/VirtualBox VMs/go-virtualbox/ubuntu-16.04-amd64-disk001.vmdk" 73 | "SATA Controller-ImageUUID-0-0"="32583b48-693e-45d4-882f-e9196d4f43c6" 74 | natnet1="nat" 75 | macaddress1="080027EE1DF7" 76 | cableconnected1="on" 77 | nic1="nat" 78 | nictype1="82540EM" 79 | nicspeed1="0" 80 | mtu="0" 81 | sockSnd="64" 82 | sockRcv="64" 83 | tcpWndSnd="64" 84 | tcpWndRcv="64" 85 | Forwarding(0)="ssh,tcp,127.0.0.1,2222,,22" 86 | nic2="none" 87 | nic3="none" 88 | nic4="none" 89 | nic5="none" 90 | nic6="none" 91 | nic7="none" 92 | nic8="none" 93 | hidpointing="ps2mouse" 94 | hidkeyboard="ps2kbd" 95 | uart1="off" 96 | uart2="off" 97 | uart3="off" 98 | uart4="off" 99 | lpt1="off" 100 | lpt2="off" 101 | audio="coreaudio" 102 | clipboard="disabled" 103 | draganddrop="disabled" 104 | vrde="on" 105 | vrdeport=-1 106 | vrdeports="5914" 107 | vrdeaddress="127.0.0.1" 108 | vrdeauthtype="null" 109 | vrdemulticon="off" 110 | vrdereusecon="off" 111 | vrdevideochannel="off" 112 | vrdeproperty[TCP/Ports]="5914" 113 | vrdeproperty[TCP/Address]="127.0.0.1" 114 | vrdeproperty[VideoChannel/Enabled]= 115 | vrdeproperty[VideoChannel/Quality]= 116 | vrdeproperty[VideoChannel/DownscaleProtection]= 117 | vrdeproperty[Client/DisableDisplay]= 118 | vrdeproperty[Client/DisableInput]= 119 | vrdeproperty[Client/DisableAudio]= 120 | vrdeproperty[Client/DisableUSB]= 121 | vrdeproperty[Client/DisableClipboard]= 122 | vrdeproperty[Client/DisableUpstreamAudio]= 123 | vrdeproperty[Client/DisableRDPDR]= 124 | vrdeproperty[H3DRedirect/Enabled]= 125 | vrdeproperty[Security/Method]= 126 | vrdeproperty[Security/ServerCertificate]= 127 | vrdeproperty[Security/ServerPrivateKey]= 128 | vrdeproperty[Security/CACertificate]= 129 | vrdeproperty[Audio/RateCorrectionMode]= 130 | vrdeproperty[Audio/LogPath]= 131 | usb="off" 132 | ehci="off" 133 | xhci="off" 134 | SharedFolderNameMachineMapping1="vagrant" 135 | SharedFolderPathMachineMapping1="/Users/fix/Desktop/GO/src/github.com/terra-farm/go-virtualbox" 136 | vcpenabled="off" 137 | vcpscreens=0 138 | vcpfile="/Users/fix/VirtualBox VMs/go-virtualbox/go-virtualbox.webm" 139 | vcpwidth=1024 140 | vcpheight=768 141 | vcprate=512 142 | vcpfps=25 143 | GuestMemoryBalloon=0 144 | -------------------------------------------------------------------------------- /testdata/showvminfo_go-virtualbox_--machinereadable.out: -------------------------------------------------------------------------------- 1 | name="go-virtualbox" 2 | groups="/" 3 | ostype="Ubuntu (64-bit)" 4 | UUID="37f5d336-bf08-48dd-947c-37e6a56420a7" 5 | CfgFile="/Users/fix/VirtualBox VMs/go-virtualbox/go-virtualbox.vbox" 6 | SnapFldr="/Users/fix/VirtualBox VMs/go-virtualbox/Snapshots" 7 | LogFldr="/Users/fix/VirtualBox VMs/go-virtualbox/Logs" 8 | hardwareuuid="37f5d336-bf07-48dd-947c-37e6a56420a7" 9 | memory=1024 10 | pagefusion="off" 11 | vram=8 12 | cpuexecutioncap=100 13 | hpet="off" 14 | chipset="piix3" 15 | firmware="BIOS" 16 | cpus=1 17 | pae="on" 18 | longmode="on" 19 | triplefaultreset="off" 20 | apic="on" 21 | x2apic="on" 22 | cpuid-portability-level=0 23 | bootmenu="messageandmenu" 24 | boot1="disk" 25 | boot2="dvd" 26 | boot3="none" 27 | boot4="none" 28 | acpi="on" 29 | ioapic="on" 30 | biosapic="apic" 31 | biossystemtimeoffset=0 32 | rtcuseutc="on" 33 | hwvirtex="on" 34 | nestedpaging="on" 35 | largepages="on" 36 | vtxvpid="on" 37 | vtxux="on" 38 | paravirtprovider="default" 39 | effparavirtprovider="kvm" 40 | VMState="saved" 41 | VMStateChangeTime="2018-04-23T09:29:53.476000000" 42 | VMStateFile="/Users/fix/VirtualBox VMs/go-virtualbox/Snapshots/2018-04-23T09-29-48-014952000Z.sav" 43 | monitorcount=1 44 | accelerate3d="off" 45 | accelerate2dvideo="off" 46 | teleporterenabled="off" 47 | teleporterport=0 48 | teleporteraddress="" 49 | teleporterpassword="" 50 | tracing-enabled="off" 51 | tracing-allow-vm-access="off" 52 | tracing-config="" 53 | autostart-enabled="off" 54 | autostart-delay=0 55 | defaultfrontend="" 56 | storagecontrollername0="IDE Controller" 57 | storagecontrollertype0="PIIX4" 58 | storagecontrollerinstance0="0" 59 | storagecontrollermaxportcount0="2" 60 | storagecontrollerportcount0="2" 61 | storagecontrollerbootable0="on" 62 | storagecontrollername1="SATA Controller" 63 | storagecontrollertype1="IntelAhci" 64 | storagecontrollerinstance1="0" 65 | storagecontrollermaxportcount1="30" 66 | storagecontrollerportcount1="1" 67 | storagecontrollerbootable1="on" 68 | "IDE Controller-0-0"="none" 69 | "IDE Controller-0-1"="none" 70 | "IDE Controller-1-0"="none" 71 | "IDE Controller-1-1"="none" 72 | "SATA Controller-0-0"="/Users/fix/VirtualBox VMs/go-virtualbox/ubuntu-16.04-amd64-disk001.vmdk" 73 | "SATA Controller-ImageUUID-0-0"="32583b48-693e-45d4-882f-e9196d4f43c6" 74 | natnet1="nat" 75 | macaddress1="080027EE1DF7" 76 | cableconnected1="on" 77 | nic1="nat" 78 | nictype1="82540EM" 79 | nicspeed1="0" 80 | mtu="0" 81 | sockSnd="64" 82 | sockRcv="64" 83 | tcpWndSnd="64" 84 | tcpWndRcv="64" 85 | Forwarding(0)="ssh,tcp,127.0.0.1,2222,,22" 86 | nic2="none" 87 | nic3="none" 88 | nic4="none" 89 | nic5="none" 90 | nic6="none" 91 | nic7="none" 92 | nic8="none" 93 | hidpointing="ps2mouse" 94 | hidkeyboard="ps2kbd" 95 | uart1="off" 96 | uart2="off" 97 | uart3="off" 98 | uart4="off" 99 | lpt1="off" 100 | lpt2="off" 101 | audio="coreaudio" 102 | clipboard="disabled" 103 | draganddrop="disabled" 104 | vrde="on" 105 | vrdeport=-1 106 | vrdeports="5914" 107 | vrdeaddress="127.0.0.1" 108 | vrdeauthtype="null" 109 | vrdemulticon="off" 110 | vrdereusecon="off" 111 | vrdevideochannel="off" 112 | vrdeproperty[TCP/Ports]="5914" 113 | vrdeproperty[TCP/Address]="127.0.0.1" 114 | vrdeproperty[VideoChannel/Enabled]= 115 | vrdeproperty[VideoChannel/Quality]= 116 | vrdeproperty[VideoChannel/DownscaleProtection]= 117 | vrdeproperty[Client/DisableDisplay]= 118 | vrdeproperty[Client/DisableInput]= 119 | vrdeproperty[Client/DisableAudio]= 120 | vrdeproperty[Client/DisableUSB]= 121 | vrdeproperty[Client/DisableClipboard]= 122 | vrdeproperty[Client/DisableUpstreamAudio]= 123 | vrdeproperty[Client/DisableRDPDR]= 124 | vrdeproperty[H3DRedirect/Enabled]= 125 | vrdeproperty[Security/Method]= 126 | vrdeproperty[Security/ServerCertificate]= 127 | vrdeproperty[Security/ServerPrivateKey]= 128 | vrdeproperty[Security/CACertificate]= 129 | vrdeproperty[Audio/RateCorrectionMode]= 130 | vrdeproperty[Audio/LogPath]= 131 | usb="off" 132 | ehci="off" 133 | xhci="off" 134 | SharedFolderNameMachineMapping1="vagrant" 135 | SharedFolderPathMachineMapping1="/Users/fix/Desktop/GO/src/github.com/terra-farm/go-virtualbox" 136 | vcpenabled="off" 137 | vcpscreens=0 138 | vcpfile="/Users/fix/VirtualBox VMs/go-virtualbox/go-virtualbox.webm" 139 | vcpwidth=1024 140 | vcpheight=768 141 | vcprate=512 142 | vcpfps=25 143 | GuestMemoryBalloon=0 144 | -------------------------------------------------------------------------------- /testdata/vboxmanage-guestproperty-wait-1.out: -------------------------------------------------------------------------------- 1 | 2 | Name: test_key, value: test_val1, flags: -------------------------------------------------------------------------------- /testdata/vboxmanage-guestproperty-wait-2.out: -------------------------------------------------------------------------------- 1 | 2 | Name: test_key, value: test_val1, flags: -------------------------------------------------------------------------------- /testdata/vboxmanage-list-dhcpservers-1.out: -------------------------------------------------------------------------------- 1 | NetworkName: HostInterfaceNetworking-VirtualBox Host-Only Ethernet Adapter 2 | IP: 192.168.56.100 3 | NetworkMask: 255.255.255.0 4 | lowerIPAddress: 192.168.56.101 5 | upperIPAddress: 192.168.56.254 6 | Enabled: Yes 7 | -------------------------------------------------------------------------------- /testdata/vboxmanage-list-hostonlyifs-1.out: -------------------------------------------------------------------------------- 1 | Name: vboxnet0 2 | GUID: 786f6276-656e-4074-8000-0a0027000000 3 | DHCP: Disabled 4 | IPAddress: 192.168.56.1 5 | NetworkMask: 255.255.255.0 6 | IPV6Address: 7 | IPV6NetworkMaskPrefixLength: 0 8 | HardwareAddress: 0a:00:27:00:00:00 9 | MediumType: Ethernet 10 | Status: Down 11 | VBoxNetworkName: HostInterfaceNetworking-vboxnet0 12 | -------------------------------------------------------------------------------- /testdata/vboxmanage-list-natnets-1.out: -------------------------------------------------------------------------------- 1 | NetworkName: NatNetwork 2 | IP: 10.0.2.1 3 | Network: 10.0.2.0/24 4 | IPv6 Enabled: No 5 | IPv6 Prefix: fd17:625c:f037:2::/64 6 | DHCP Enabled: Yes 7 | Enabled: Yes 8 | loopback mappings (ipv4) 9 | 127.0.0.1=2 10 | -------------------------------------------------------------------------------- /testdata/vboxmanage-list-vms-1.out: -------------------------------------------------------------------------------- 1 | "Ubuntu" {2e16b1fc-675d-4a7a-a9a1-e89a8bde7874} 2 | "go-virtualbox" {def44546-e3da-4902-8d15-b91c99c80cbc} -------------------------------------------------------------------------------- /testdata/vboxmanage-showvminfo-1.out: -------------------------------------------------------------------------------- 1 | name="go-virtualbox" 2 | groups="/" 3 | ostype="Ubuntu (64-bit)" 4 | UUID="37f5d336-bf07-48dd-947c-37e6a56420a7" 5 | CfgFile="/Users/fix/VirtualBox VMs/go-virtualbox/go-virtualbox.vbox" 6 | SnapFldr="/Users/fix/VirtualBox VMs/go-virtualbox/Snapshots" 7 | LogFldr="/Users/fix/VirtualBox VMs/go-virtualbox/Logs" 8 | hardwareuuid="37f5d336-bf07-48dd-947c-37e6a56420a7" 9 | memory=1024 10 | pagefusion="off" 11 | vram=8 12 | cpuexecutioncap=100 13 | hpet="off" 14 | chipset="piix3" 15 | firmware="BIOS" 16 | cpus=1 17 | pae="on" 18 | longmode="on" 19 | triplefaultreset="off" 20 | apic="on" 21 | x2apic="on" 22 | cpuid-portability-level=0 23 | bootmenu="messageandmenu" 24 | boot1="disk" 25 | boot2="dvd" 26 | boot3="none" 27 | boot4="none" 28 | acpi="on" 29 | ioapic="on" 30 | biosapic="apic" 31 | biossystemtimeoffset=0 32 | rtcuseutc="on" 33 | hwvirtex="on" 34 | nestedpaging="on" 35 | largepages="on" 36 | vtxvpid="on" 37 | vtxux="on" 38 | paravirtprovider="default" 39 | effparavirtprovider="kvm" 40 | VMState="saved" 41 | VMStateChangeTime="2018-04-23T09:29:53.476000000" 42 | VMStateFile="/Users/fix/VirtualBox VMs/go-virtualbox/Snapshots/2018-04-23T09-29-48-014952000Z.sav" 43 | monitorcount=1 44 | accelerate3d="off" 45 | accelerate2dvideo="off" 46 | teleporterenabled="off" 47 | teleporterport=0 48 | teleporteraddress="" 49 | teleporterpassword="" 50 | tracing-enabled="off" 51 | tracing-allow-vm-access="off" 52 | tracing-config="" 53 | autostart-enabled="off" 54 | autostart-delay=0 55 | defaultfrontend="" 56 | storagecontrollername0="IDE Controller" 57 | storagecontrollertype0="PIIX4" 58 | storagecontrollerinstance0="0" 59 | storagecontrollermaxportcount0="2" 60 | storagecontrollerportcount0="2" 61 | storagecontrollerbootable0="on" 62 | storagecontrollername1="SATA Controller" 63 | storagecontrollertype1="IntelAhci" 64 | storagecontrollerinstance1="0" 65 | storagecontrollermaxportcount1="30" 66 | storagecontrollerportcount1="1" 67 | storagecontrollerbootable1="on" 68 | "IDE Controller-0-0"="none" 69 | "IDE Controller-0-1"="none" 70 | "IDE Controller-1-0"="none" 71 | "IDE Controller-1-1"="none" 72 | "SATA Controller-0-0"="/Users/fix/VirtualBox VMs/go-virtualbox/ubuntu-16.04-amd64-disk001.vmdk" 73 | "SATA Controller-ImageUUID-0-0"="32583b48-693e-45d4-882f-e9196d4f43c6" 74 | natnet1="nat" 75 | macaddress1="080027EE1DF7" 76 | cableconnected1="on" 77 | nic1="nat" 78 | nictype1="82540EM" 79 | nicspeed1="0" 80 | mtu="0" 81 | sockSnd="64" 82 | sockRcv="64" 83 | tcpWndSnd="64" 84 | tcpWndRcv="64" 85 | Forwarding(0)="ssh,tcp,127.0.0.1,2222,,22" 86 | nic2="none" 87 | nic3="none" 88 | nic4="none" 89 | nic5="none" 90 | nic6="none" 91 | nic7="none" 92 | nic8="none" 93 | hidpointing="ps2mouse" 94 | hidkeyboard="ps2kbd" 95 | uart1="off" 96 | uart2="off" 97 | uart3="off" 98 | uart4="off" 99 | lpt1="off" 100 | lpt2="off" 101 | audio="coreaudio" 102 | clipboard="disabled" 103 | draganddrop="disabled" 104 | vrde="on" 105 | vrdeport=-1 106 | vrdeports="5914" 107 | vrdeaddress="127.0.0.1" 108 | vrdeauthtype="null" 109 | vrdemulticon="off" 110 | vrdereusecon="off" 111 | vrdevideochannel="off" 112 | vrdeproperty[TCP/Ports]="5914" 113 | vrdeproperty[TCP/Address]="127.0.0.1" 114 | vrdeproperty[VideoChannel/Enabled]= 115 | vrdeproperty[VideoChannel/Quality]= 116 | vrdeproperty[VideoChannel/DownscaleProtection]= 117 | vrdeproperty[Client/DisableDisplay]= 118 | vrdeproperty[Client/DisableInput]= 119 | vrdeproperty[Client/DisableAudio]= 120 | vrdeproperty[Client/DisableUSB]= 121 | vrdeproperty[Client/DisableClipboard]= 122 | vrdeproperty[Client/DisableUpstreamAudio]= 123 | vrdeproperty[Client/DisableRDPDR]= 124 | vrdeproperty[H3DRedirect/Enabled]= 125 | vrdeproperty[Security/Method]= 126 | vrdeproperty[Security/ServerCertificate]= 127 | vrdeproperty[Security/ServerPrivateKey]= 128 | vrdeproperty[Security/CACertificate]= 129 | vrdeproperty[Audio/RateCorrectionMode]= 130 | vrdeproperty[Audio/LogPath]= 131 | usb="off" 132 | ehci="off" 133 | xhci="off" 134 | SharedFolderNameMachineMapping1="vagrant" 135 | SharedFolderPathMachineMapping1="/Users/fix/Desktop/GO/src/github.com/terra-farm/go-virtualbox" 136 | vcpenabled="off" 137 | vcpscreens=0 138 | vcpfile="/Users/fix/VirtualBox VMs/go-virtualbox/go-virtualbox.webm" 139 | vcpwidth=1024 140 | vcpheight=768 141 | vcprate=512 142 | vcpfps=25 143 | GuestMemoryBalloon=0 144 | -------------------------------------------------------------------------------- /testdata/vboxmanage-showvminfo-2.out: -------------------------------------------------------------------------------- 1 | name="go-virtualbox" 2 | groups="/" 3 | ostype="Ubuntu (64-bit)" 4 | UUID="37f5d336-bf07-48dd-947c-37e6a56420a7" 5 | CfgFile="/Users/fix/VirtualBox VMs/go-virtualbox/go-virtualbox.vbox" 6 | SnapFldr="/Users/fix/VirtualBox VMs/go-virtualbox/Snapshots" 7 | LogFldr="/Users/fix/VirtualBox VMs/go-virtualbox/Logs" 8 | hardwareuuid="37f5d336-bf07-48dd-947c-37e6a56420a7" 9 | memory=1024 10 | pagefusion="off" 11 | vram=8 12 | cpuexecutioncap=100 13 | hpet="off" 14 | chipset="piix3" 15 | firmware="BIOS" 16 | cpus=1 17 | pae="on" 18 | longmode="on" 19 | triplefaultreset="off" 20 | apic="on" 21 | x2apic="on" 22 | cpuid-portability-level=0 23 | bootmenu="messageandmenu" 24 | boot1="disk" 25 | boot2="dvd" 26 | boot3="none" 27 | boot4="none" 28 | acpi="on" 29 | ioapic="on" 30 | biosapic="apic" 31 | biossystemtimeoffset=0 32 | rtcuseutc="on" 33 | hwvirtex="on" 34 | nestedpaging="on" 35 | largepages="on" 36 | vtxvpid="on" 37 | vtxux="on" 38 | paravirtprovider="default" 39 | effparavirtprovider="kvm" 40 | VMState="saved" 41 | VMStateChangeTime="2018-04-23T09:29:53.476000000" 42 | VMStateFile="/Users/fix/VirtualBox VMs/go-virtualbox/Snapshots/2018-04-23T09-29-48-014952000Z.sav" 43 | monitorcount=1 44 | accelerate3d="off" 45 | accelerate2dvideo="off" 46 | teleporterenabled="off" 47 | teleporterport=0 48 | teleporteraddress="" 49 | teleporterpassword="" 50 | tracing-enabled="off" 51 | tracing-allow-vm-access="off" 52 | tracing-config="" 53 | autostart-enabled="off" 54 | autostart-delay=0 55 | defaultfrontend="" 56 | storagecontrollername0="IDE Controller" 57 | storagecontrollertype0="PIIX4" 58 | storagecontrollerinstance0="0" 59 | storagecontrollermaxportcount0="2" 60 | storagecontrollerportcount0="2" 61 | storagecontrollerbootable0="on" 62 | storagecontrollername1="SATA Controller" 63 | storagecontrollertype1="IntelAhci" 64 | storagecontrollerinstance1="0" 65 | storagecontrollermaxportcount1="30" 66 | storagecontrollerportcount1="1" 67 | storagecontrollerbootable1="on" 68 | "IDE Controller-0-0"="none" 69 | "IDE Controller-0-1"="none" 70 | "IDE Controller-1-0"="none" 71 | "IDE Controller-1-1"="none" 72 | "SATA Controller-0-0"="/Users/fix/VirtualBox VMs/go-virtualbox/ubuntu-16.04-amd64-disk001.vmdk" 73 | "SATA Controller-ImageUUID-0-0"="32583b48-693e-45d4-882f-e9196d4f43c6" 74 | natnet1="nat" 75 | macaddress1="080027EE1DF7" 76 | cableconnected1="on" 77 | nic1="nat" 78 | nictype1="82540EM" 79 | nicspeed1="0" 80 | mtu="0" 81 | sockSnd="64" 82 | sockRcv="64" 83 | tcpWndSnd="64" 84 | tcpWndRcv="64" 85 | Forwarding(0)="ssh,tcp,127.0.0.1,2222,,22" 86 | nic2="none" 87 | nic3="none" 88 | nic4="none" 89 | nic5="none" 90 | nic6="none" 91 | nic7="none" 92 | nic8="none" 93 | hidpointing="ps2mouse" 94 | hidkeyboard="ps2kbd" 95 | uart1="off" 96 | uart2="off" 97 | uart3="off" 98 | uart4="off" 99 | lpt1="off" 100 | lpt2="off" 101 | audio="coreaudio" 102 | clipboard="disabled" 103 | draganddrop="disabled" 104 | vrde="on" 105 | vrdeport=-1 106 | vrdeports="5914" 107 | vrdeaddress="127.0.0.1" 108 | vrdeauthtype="null" 109 | vrdemulticon="off" 110 | vrdereusecon="off" 111 | vrdevideochannel="off" 112 | vrdeproperty[TCP/Ports]="5914" 113 | vrdeproperty[TCP/Address]="127.0.0.1" 114 | vrdeproperty[VideoChannel/Enabled]= 115 | vrdeproperty[VideoChannel/Quality]= 116 | vrdeproperty[VideoChannel/DownscaleProtection]= 117 | vrdeproperty[Client/DisableDisplay]= 118 | vrdeproperty[Client/DisableInput]= 119 | vrdeproperty[Client/DisableAudio]= 120 | vrdeproperty[Client/DisableUSB]= 121 | vrdeproperty[Client/DisableClipboard]= 122 | vrdeproperty[Client/DisableUpstreamAudio]= 123 | vrdeproperty[Client/DisableRDPDR]= 124 | vrdeproperty[H3DRedirect/Enabled]= 125 | vrdeproperty[Security/Method]= 126 | vrdeproperty[Security/ServerCertificate]= 127 | vrdeproperty[Security/ServerPrivateKey]= 128 | vrdeproperty[Security/CACertificate]= 129 | vrdeproperty[Audio/RateCorrectionMode]= 130 | vrdeproperty[Audio/LogPath]= 131 | usb="off" 132 | ehci="off" 133 | xhci="off" 134 | SharedFolderNameMachineMapping1="vagrant" 135 | SharedFolderPathMachineMapping1="/Users/fix/Desktop/GO/src/github.com/terra-farm/go-virtualbox" 136 | vcpenabled="off" 137 | vcpscreens=0 138 | vcpfile="/Users/fix/VirtualBox VMs/go-virtualbox/go-virtualbox.webm" 139 | vcpwidth=1024 140 | vcpheight=768 141 | vcprate=512 142 | vcpfps=25 143 | GuestMemoryBalloon=0 -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package virtualbox 2 | 3 | import ( 4 | "context" 5 | "net" 6 | ) 7 | 8 | // ParseIPv4Mask parses IPv4 netmask written in IP form (e.g. 255.255.255.0). 9 | // This function should really belong to the net package. 10 | func ParseIPv4Mask(s string) net.IPMask { 11 | mask := net.ParseIP(s) 12 | if mask == nil { 13 | return nil 14 | } 15 | return net.IPv4Mask(mask[12], mask[13], mask[14], mask[15]) 16 | } 17 | 18 | // Run is a helper method used to execute the commands using the configured 19 | // VBoxManage path. The command should be omitted and only the arguments 20 | // should be passed. It will return the stdout, stderr and error if one 21 | // occured during command execution. 22 | func Run(_ context.Context, args ...string) (string, string, error) { 23 | // TODO: Convert the function so you can pass in the context. 24 | return Manage().run(args...) 25 | } 26 | -------------------------------------------------------------------------------- /vbcmd.go: -------------------------------------------------------------------------------- 1 | // DEPRECATED: Use Virtualbox and other interfaces 2 | //go:generate go run github.com/golang/mock/mockgen@latest -source=vbcmd.go -destination=vbcmd.mock.go -package=virtualbox -mock_names=Command=MockCommand 3 | 4 | package virtualbox 5 | 6 | import ( 7 | "bytes" 8 | "errors" 9 | "os/exec" 10 | "runtime" 11 | ) 12 | 13 | type option func(Command) 14 | 15 | // Command is the mock-able interface to run VirtualBox commands 16 | // such as VBoxManage (host side) or VBoxControl (guest side) 17 | type Command interface { 18 | setOpts(opts ...option) Command 19 | isGuest() bool 20 | path() string 21 | run(args ...string) (string, string, error) 22 | } 23 | 24 | var ( 25 | // Verbose toggles the library in verbose execution mode. 26 | Verbose bool 27 | // ErrMachineExist holds the error message when the machine already exists. 28 | ErrMachineExist = errors.New("machine already exists") 29 | // ErrMachineNotExist holds the error message when the machine does not exist. 30 | ErrMachineNotExist = errors.New("machine does not exist") 31 | // ErrCommandNotFound holds the error message when the VBoxManage commands was not found. 32 | ErrCommandNotFound = errors.New("command not found") 33 | ) 34 | 35 | type command struct { 36 | program string 37 | sudoer bool // Is current user a sudoer? 38 | sudo bool // Is current command expected to be run under sudo? 39 | guest bool 40 | } 41 | 42 | func (vbcmd command) setOpts(opts ...option) Command { 43 | var cmd Command = &vbcmd 44 | for _, opt := range opts { 45 | opt(cmd) 46 | } 47 | return cmd 48 | } 49 | 50 | func sudo(sudo bool) option { 51 | return func(cmd Command) { 52 | vbcmd := cmd.(*command) 53 | vbcmd.sudo = sudo 54 | Debug("Next sudo: %v", vbcmd.sudo) 55 | } 56 | } 57 | 58 | func (vbcmd command) isGuest() bool { 59 | return vbcmd.guest 60 | } 61 | 62 | func (vbcmd command) path() string { 63 | return vbcmd.program 64 | } 65 | 66 | func (vbcmd command) prepare(args []string) *exec.Cmd { 67 | program := vbcmd.program 68 | argv := []string{} 69 | Debug("Command: '%+v', runtime.GOOS: '%s'", vbcmd, runtime.GOOS) 70 | if vbcmd.sudoer && vbcmd.sudo && runtime.GOOS != osWindows { 71 | program = "sudo" 72 | argv = append(argv, vbcmd.program) 73 | } 74 | argv = append(argv, args...) 75 | Debug("executing: %v %v", program, argv) 76 | return exec.Command(program, argv...) // #nosec 77 | } 78 | 79 | func (vbcmd command) run(args ...string) (string, string, error) { 80 | defer vbcmd.setOpts(sudo(false)) 81 | cmd := vbcmd.prepare(args) 82 | var stdout bytes.Buffer 83 | var stderr bytes.Buffer 84 | cmd.Stdout = &stdout 85 | cmd.Stderr = &stderr 86 | err := cmd.Run() 87 | if err != nil { 88 | if ee, ok := err.(*exec.Error); ok && ee == exec.ErrNotFound { 89 | err = ErrCommandNotFound 90 | } 91 | } 92 | return stdout.String(), stderr.String(), err 93 | } 94 | -------------------------------------------------------------------------------- /vbcmd.mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: vbcmd.go 3 | 4 | // Package virtualbox is a generated GoMock package. 5 | package virtualbox 6 | 7 | import ( 8 | reflect "reflect" 9 | 10 | gomock "github.com/golang/mock/gomock" 11 | ) 12 | 13 | // MockCommand is a mock of Command interface. 14 | type MockCommand struct { 15 | ctrl *gomock.Controller 16 | recorder *MockCommandMockRecorder 17 | } 18 | 19 | // MockCommandMockRecorder is the mock recorder for MockCommand. 20 | type MockCommandMockRecorder struct { 21 | mock *MockCommand 22 | } 23 | 24 | // NewMockCommand creates a new mock instance. 25 | func NewMockCommand(ctrl *gomock.Controller) *MockCommand { 26 | mock := &MockCommand{ctrl: ctrl} 27 | mock.recorder = &MockCommandMockRecorder{mock} 28 | return mock 29 | } 30 | 31 | // EXPECT returns an object that allows the caller to indicate expected use. 32 | func (m *MockCommand) EXPECT() *MockCommandMockRecorder { 33 | return m.recorder 34 | } 35 | 36 | // isGuest mocks base method. 37 | func (m *MockCommand) isGuest() bool { 38 | m.ctrl.T.Helper() 39 | ret := m.ctrl.Call(m, "isGuest") 40 | ret0, _ := ret[0].(bool) 41 | return ret0 42 | } 43 | 44 | // isGuest indicates an expected call of isGuest. 45 | func (mr *MockCommandMockRecorder) isGuest() *gomock.Call { 46 | mr.mock.ctrl.T.Helper() 47 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "isGuest", reflect.TypeOf((*MockCommand)(nil).isGuest)) 48 | } 49 | 50 | // path mocks base method. 51 | func (m *MockCommand) path() string { 52 | m.ctrl.T.Helper() 53 | ret := m.ctrl.Call(m, "path") 54 | ret0, _ := ret[0].(string) 55 | return ret0 56 | } 57 | 58 | // path indicates an expected call of path. 59 | func (mr *MockCommandMockRecorder) path() *gomock.Call { 60 | mr.mock.ctrl.T.Helper() 61 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "path", reflect.TypeOf((*MockCommand)(nil).path)) 62 | } 63 | 64 | // run mocks base method. 65 | func (m *MockCommand) run(args ...string) (string, string, error) { 66 | m.ctrl.T.Helper() 67 | varargs := []interface{}{} 68 | for _, a := range args { 69 | varargs = append(varargs, a) 70 | } 71 | ret := m.ctrl.Call(m, "run", varargs...) 72 | ret0, _ := ret[0].(string) 73 | ret1, _ := ret[1].(string) 74 | ret2, _ := ret[2].(error) 75 | return ret0, ret1, ret2 76 | } 77 | 78 | // run indicates an expected call of run. 79 | func (mr *MockCommandMockRecorder) run(args ...interface{}) *gomock.Call { 80 | mr.mock.ctrl.T.Helper() 81 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "run", reflect.TypeOf((*MockCommand)(nil).run), args...) 82 | } 83 | 84 | // setOpts mocks base method. 85 | func (m *MockCommand) setOpts(opts ...option) Command { 86 | m.ctrl.T.Helper() 87 | varargs := []interface{}{} 88 | for _, a := range opts { 89 | varargs = append(varargs, a) 90 | } 91 | ret := m.ctrl.Call(m, "setOpts", varargs...) 92 | ret0, _ := ret[0].(Command) 93 | return ret0 94 | } 95 | 96 | // setOpts indicates an expected call of setOpts. 97 | func (mr *MockCommandMockRecorder) setOpts(opts ...interface{}) *gomock.Call { 98 | mr.mock.ctrl.T.Helper() 99 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "setOpts", reflect.TypeOf((*MockCommand)(nil).setOpts), opts...) 100 | } 101 | -------------------------------------------------------------------------------- /vbmgt.go: -------------------------------------------------------------------------------- 1 | package virtualbox 2 | 3 | import ( 4 | "os" 5 | "os/exec" 6 | "os/user" 7 | "path/filepath" 8 | "regexp" 9 | "runtime" 10 | ) 11 | 12 | var ( 13 | manage Command 14 | ) 15 | 16 | var ( 17 | reVMNameUUID = regexp.MustCompile(`"(.+)" {([0-9a-f-]+)}`) 18 | reVMInfoLine = regexp.MustCompile(`(?:"(.+)"|(.+))=(?:"(.*)"|(.*))`) 19 | reColonLine = regexp.MustCompile(`(.+):\s+(.*)`) 20 | reMachineNotFound = regexp.MustCompile(`Could not find a registered machine named '(.+)'`) 21 | ) 22 | 23 | // Manage returns the Command to run VBoxManage/VBoxControl. 24 | func Manage() Command { 25 | if manage != nil { 26 | return manage 27 | } 28 | 29 | sudoer, err := isSudoer() 30 | if err != nil { 31 | Debug("Error getting sudoer status: '%v'", err) 32 | } 33 | 34 | if vbprog, err := lookupVBoxProgram("VBoxManage"); err == nil { 35 | manage = command{program: vbprog, sudoer: sudoer, guest: false} 36 | } else if vbprog, err := lookupVBoxProgram("VBoxControl"); err == nil { 37 | manage = command{program: vbprog, sudoer: sudoer, guest: true} 38 | } else { 39 | // Did not find a VirtualBox management command 40 | manage = command{program: "false", sudoer: false, guest: false} 41 | } 42 | Debug("manage: '%+v'", manage) 43 | return manage 44 | } 45 | 46 | func lookupVBoxProgram(vbprog string) (string, error) { 47 | 48 | if runtime.GOOS == osWindows { 49 | if p := os.Getenv("VBOX_INSTALL_PATH"); p != "" { 50 | vbprog = filepath.Join(p, vbprog+".exe") 51 | } else { 52 | vbprog = filepath.Join("C:\\", "Program Files", "Oracle", "VirtualBox", vbprog+".exe") 53 | } 54 | } 55 | 56 | return exec.LookPath(vbprog) 57 | } 58 | 59 | func isSudoer() (bool, error) { 60 | me, err := user.Current() 61 | if err != nil { 62 | return false, err 63 | } 64 | Debug("User: '%+v'", me) 65 | if groupIDs, err := me.GroupIds(); runtime.GOOS == "linux" { 66 | if err != nil { 67 | return false, err 68 | } 69 | Debug("groupIDs: '%+v'", groupIDs) 70 | for _, groupID := range groupIDs { 71 | group, err := user.LookupGroupId(groupID) 72 | if err != nil { 73 | return false, err 74 | } 75 | Debug("group: '%+v'", group) 76 | if group.Name == "sudo" { 77 | return true, nil 78 | } 79 | } 80 | } 81 | return false, nil 82 | } 83 | -------------------------------------------------------------------------------- /vbmgt_test.go: -------------------------------------------------------------------------------- 1 | package virtualbox 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path" 7 | "testing" 8 | 9 | "github.com/golang/mock/gomock" 10 | ) 11 | 12 | var ( 13 | MockCtrl *gomock.Controller 14 | ManageMock *MockCommand 15 | VM string 16 | TestDataFolder string 17 | ) 18 | 19 | func ReadTestData(file string) string { 20 | out, err := ioutil.ReadFile(path.Join("testdata", file)) 21 | if err != nil { 22 | panic("No such file :testdata/" + file) 23 | } 24 | return string(out) 25 | } 26 | 27 | func Setup(t *testing.T) { 28 | VM = os.Getenv("TEST_VM") 29 | MockCtrl = gomock.NewController(t) 30 | if len(VM) < 1 { 31 | ManageMock = NewMockCommand(MockCtrl) 32 | manage = ManageMock 33 | t.Logf("Using ManageMock=%v (type=%T)", ManageMock, ManageMock) 34 | } else { 35 | t.Logf("Using real VM='%s'\n", VM) 36 | } 37 | t.Logf("Using Manage='%+v' (type: '%T')", Manage(), Manage()) 38 | } 39 | 40 | func Teardown() { 41 | defer MockCtrl.Finish() 42 | } 43 | 44 | func TestVBMOut(t *testing.T) { 45 | Setup(t) 46 | 47 | if ManageMock != nil { 48 | var out = "\"Ubuntu\" {2e16b1fc-aaaa-4a7a-a9a1-e89a8bde7874}\n" + 49 | "\"go-virtualbox\" {def44546-aaaa-4902-8d15-b91c99c80cbc}" 50 | ManageMock.EXPECT().run("list", "vms").Return(out, "", nil) 51 | } 52 | b, _, err := Manage().run("list", "vms") 53 | if err != nil { 54 | t.Fatal(err) 55 | } 56 | t.Logf("%s", b) 57 | 58 | Teardown() 59 | } 60 | --------------------------------------------------------------------------------