diff --git a/README.md b/README.md index cc34709..bd320d2 100644 --- a/README.md +++ b/README.md @@ -96,15 +96,16 @@ the following options (undefined options take default values): hostip=192.168.59.3 # host-only network host IP netmask=255.255.255.0 # host only network network mask dhcpip=192.168.59.99 # host-only network DHCP server IP - dhcp=Yes # host-only network DHCP server enabled + dhcp=true # host-only network DHCP server enabled lowerip=192.168.59.103 # host-only network IP range lower bound upperip=192.168.59.254 # host-only network IP range upper bound Environment variables of the form `$ENVVAR` in the profile will be expanded, even on Windows. -You can override the configurations using command line flags. Type -`boot2docker-cli -h` for more information. +You can override the configurations using command-line flags. Type +`boot2docker-cli -h` for more information. The configuration file options are +the same as the command-line flags with long names. diff --git a/cmds.go b/cmds.go index d7ceb59..ddfaa1e 100644 --- a/cmds.go +++ b/cmds.go @@ -1,90 +1,21 @@ package main import ( + "encoding/json" "fmt" + "net" "os" "path/filepath" "runtime" "time" -) - -/* -VirtualBox Machine State Transition - -A VirtualBox machine can be in one of the following states: - -- poweroff: The VM is powered off and no previous running state saved. -- running: The VM is running. -- paused: The VM is paused, but its state is not saved to disk. If you quit - VirtualBox, the state will be lost. -- saved: The VM is powered off, and the previous state is saved on disk. -- aborted: The VM process crashed. This should happen very rarely. - -VBoxManage supports the following transitions between states: - -- startvm : poweroff|saved --> running -- controlvm pause: running --> paused -- controlvm resume: paused --> running -- controlvm savestate: running -> saved -- controlvm acpipowerbutton: running --> poweroff -- controlvm poweroff: running --> poweroff (unsafe) -- controlvm reset: running --> poweroff --> running (unsafe) - -Poweroff and reset are unsafe because they will lose state and might corrupt -disk image. - -To make things simpler, we do not expose the seldomly used paused state. We -define the following transitions instead: - -- up|start: poweroff|saved|paused|aborted --> running -- down|halt|stop: [paused|saved -->] running --> poweroff -- save|suspend: [paused -->] running --> saved -- restart: [paused|saved -->] running --> poweroff --> running -- poweroff: [paused|saved -->] running --> poweroff (unsafe) -- reset: [paused|saved -->] running --> poweroff --> running (unsafe) - -The takeaway is we try our best to transit the virtual machine into the state -you want it to be, and you only need to watch out for the potentially unsafe -poweroff and reset. -*/ - -// State of a virtual machine. -type vmState string - -// VM state reported by VirtualBox. Note that `VBoxManage showvminfo` prints -// slightly different strings if supplied with `--machinereadable` flag. We -// use that flag as it's easier to parse. -const ( - vmPoweroff vmState = "poweroff" - vmRunning = "running" - vmPaused = "paused" - vmSaved = "saved" - vmAborted = "aborted" -) -// The following shadow states are not actually reported by VirtualBox. We -// invented them to make handling code simpler. -const ( - vmUnregistered vmState = "(unregistered)" // No such VM registerd. - vmVBMNotFound = "(VBMNotFound)" // VBoxManage cannot be found. - vmUnknown = "(unknown)" // Any other unknown state. + vbx "github.com/boot2docker/boot2docker-cli/virtualbox" ) // Initialize the boot2docker VM from scratch. func cmdInit() int { // TODO(@riobard) break up this command into multiple stages - switch status(B2D.VM) { - case vmUnregistered: - break - case vmVBMNotFound: - logf("Failed to locate VirtualBox management utility %q", B2D.VBM) - return 2 - default: - logf("VM %q already exists.", B2D.VM) - return 1 - } - if ping(fmt.Sprintf("localhost:%d", B2D.DockerPort)) { logf("DOCKER_PORT=%d on localhost is occupied. Please choose another one.", B2D.DockerPort) return 1 @@ -107,101 +38,82 @@ func cmdInit() int { } logf("Creating VM %s...", B2D.VM) - if err := vbm("createvm", "--name", B2D.VM, "--register"); err != nil { + m, err := vbx.CreateMachine(B2D.VM, "") + if err != nil { logf("Failed to create VM %q: %s", B2D.VM, err) return 1 } logf("Apply interim patch to VM %s (https://www.virtualbox.org/ticket/12748)", B2D.VM) - if err := vbm("setextradata", B2D.VM, "VBoxInternal/CPUM/EnableHVP", "1"); err != nil { + if err := vbx.SetExtra(B2D.VM, "VBoxInternal/CPUM/EnableHVP", "1"); err != nil { logf("Failed to patch vm: %s", err) return 1 } - if err := vbm("modifyvm", B2D.VM, - "--ostype", "Linux26_64", - "--cpus", fmt.Sprintf("%d", runtime.NumCPU()), - "--memory", fmt.Sprintf("%d", B2D.Memory), - "--rtcuseutc", "on", - "--acpi", "on", - "--ioapic", "on", - "--hpet", "on", - "--hwvirtex", "on", - "--vtxvpid", "on", - "--largepages", "on", - "--nestedpaging", "on", - "--firmware", "bios", - "--bioslogofadein", "off", - "--bioslogofadeout", "off", - "--bioslogodisplaytime", "0", - "--biosbootmenu", "disabled", - "--boot1", "dvd", - ); err != nil { + m.OSType = "Linux26_64" + m.CPUs = uint(runtime.NumCPU()) + m.Memory = B2D.Memory + + m.Flag |= vbx.F_pae + m.Flag |= vbx.F_longmode // important: use x86-64 processor + m.Flag |= vbx.F_rtcuseutc + m.Flag |= vbx.F_acpi + m.Flag |= vbx.F_ioapic + m.Flag |= vbx.F_hpet + m.Flag |= vbx.F_hwvirtex + m.Flag |= vbx.F_vtxvpid + m.Flag |= vbx.F_largepages + m.Flag |= vbx.F_nestedpaging + + m.BootOrder = []string{"dvd"} + if err := m.Modify(); err != nil { logf("Failed to modify VM %q: %s", B2D.VM, err) return 1 } - logf("Setting VM networking...") - if err := vbm("modifyvm", B2D.VM, - "--nic1", "nat", - "--nictype1", "virtio", - "--cableconnected1", "on", - ); err != nil { + logf("Setting NIC #1 to use NAT network...") + if err := m.SetNIC(1, vbx.NIC{Network: vbx.NICNetNAT, Hardware: vbx.VirtIO}); err != nil { logf("Failed to add network interface to VM %q: %s", B2D.VM, err) return 1 } - if err := vbm("modifyvm", B2D.VM, - "--natpf1", fmt.Sprintf("ssh,tcp,127.0.0.1,%d,,22", B2D.SSHPort), - "--natpf1", fmt.Sprintf("docker,tcp,127.0.0.1,%d,,4243", B2D.DockerPort), - ); err != nil { - logf("Failed to add port forwarding to VM %q: %s", B2D.VM, err) - return 1 + pfRules := map[string]vbx.PFRule{ + "ssh": vbx.PFRule{Proto: vbx.PFTCP, HostIP: net.ParseIP("127.0.0.1"), HostPort: B2D.SSHPort, GuestPort: 22}, + "docker": vbx.PFRule{Proto: vbx.PFTCP, HostIP: net.ParseIP("127.0.0.1"), HostPort: B2D.DockerPort, GuestPort: 4243}, + } + + for name, rule := range pfRules { + if err := m.AddNATPF(1, name, rule); err != nil { + logf("Failed to add port forwarding to VM %q: %s", B2D.VM, err) + return 1 + } + logf("Port forwarding [%s] %s", name, rule) } - logf("Port forwarding [ssh]: host tcp://127.0.0.1:%d --> guest tcp://0.0.0.0:22", B2D.SSHPort) - logf("Port forwarding [docker]: host tcp://127.0.0.1:%d --> guest tcp://0.0.0.0:4243", B2D.DockerPort) - logf("Setting VM host-only networking") - hostifname, err := getHostOnlyNetworkInterface() + hostIFName, err := getHostOnlyNetworkInterface() if err != nil { logf("Failed to create host-only network interface: %s", err) return 1 } - logf("Adding VM host-only networking interface %s", hostifname) - if err := vbm("modifyvm", B2D.VM, - "--nic2", "hostonly", - "--nictype2", "virtio", - "--cableconnected2", "on", - "--hostonlyadapter2", hostifname, - ); err != nil { + logf("Setting NIC #2 to use host-only network %q...", hostIFName) + if err := m.SetNIC(2, vbx.NIC{Network: vbx.NICNetHostonly, Hardware: vbx.VirtIO, HostonlyAdapter: hostIFName}); err != nil { logf("Failed to add network interface to VM %q: %s", B2D.VM, err) return 1 } logf("Setting VM storage...") - if err := vbm("storagectl", B2D.VM, - "--name", "SATA", - "--add", "sata", - "--hostiocache", "on", - ); err != nil { + if err := m.AddStorageCtl("SATA", vbx.StorageController{SysBus: vbx.SysBusSATA, HostIOCache: true, Bootable: true}); err != nil { logf("Failed to add storage controller to VM %q: %s", B2D.VM, err) return 1 } - if err := vbm("storageattach", B2D.VM, - "--storagectl", "SATA", - "--port", "0", - "--device", "0", - "--type", "dvddrive", - "--medium", B2D.ISO, - ); err != nil { + if err := m.AttachStorage("SATA", vbx.StorageMedium{Port: 0, Device: 0, DriveType: vbx.DriveDVD, Medium: B2D.ISO}); err != nil { logf("Failed to attach ISO image %q: %s", B2D.ISO, err) return 1 } - vmDir := basefolder(B2D.VM) - diskImg := filepath.Join(vmDir, fmt.Sprintf("%s.vmdk", B2D.VM)) + diskImg := filepath.Join(m.BaseFolder, fmt.Sprintf("%s.vmdk", B2D.VM)) if _, err := os.Stat(diskImg); err != nil { if !os.IsNotExist(err) { @@ -215,13 +127,7 @@ func cmdInit() int { } } - if err := vbm("storageattach", B2D.VM, - "--storagectl", "SATA", - "--port", "1", - "--device", "0", - "--type", "hdd", - "--medium", diskImg, - ); err != nil { + if err := m.AttachStorage("SATA", vbx.StorageMedium{Port: 1, Device: 0, DriveType: vbx.DriveHDD, Medium: diskImg}); err != nil { logf("Failed to attach disk image %q: %s", diskImg, err) return 1 } @@ -232,38 +138,25 @@ func cmdInit() int { // Bring up the VM from all possible states. func cmdUp() int { - switch state := status(B2D.VM); state { - case vmVBMNotFound: - logf("Failed to locate VirtualBox management utility %q", B2D.VBM) + m, err := vbx.GetMachine(B2D.VM) + if err != nil { + logf("Failed to get machine %q: %s", B2D.VM, err) return 2 - case vmUnregistered: - logf("VM %q is not registered.", B2D.VM) - return 1 - case vmRunning: - logf("VM %q is already running.", B2D.VM) - case vmPaused: - logf("Resuming VM %q", B2D.VM) - if err := vbm("controlvm", B2D.VM, "resume"); err != nil { - logf("Failed to resume VM %q: %s", B2D.VM, err) - return 1 - } - case vmSaved, vmPoweroff, vmAborted: - logf("Starting VM %q...", B2D.VM) - if err := vbm("startvm", B2D.VM, "--type", "headless"); err != nil { - logf("Failed to start VM %q: %s", B2D.VM, err) - return 1 - } - default: - logf("Cannot start VM %q from state %s", B2D.VM, state) + } + if err := m.Start(); err != nil { + logf("Failed to start machine %q: %s", B2D.VM, err) return 1 } logf("Waiting for SSH server to start...") addr := fmt.Sprintf("localhost:%d", B2D.SSHPort) - if err := read(addr); err != nil { - logf("Failed to connect to SSH port at %s: %s", addr, err) + const n = 10 + // Try to connect to the SSH 10 times at 3 sec interval before giving up. + if err := read(addr, n, 3*time.Second); err != nil { + logf("Failed to connect to SSH port at %s after %d attempts. Last error: %v", addr, n, err) return 1 } + logf("Started.") switch runtime.GOOS { @@ -283,50 +176,13 @@ func cmdUp() int { // Suspend and save the current state of VM on disk. func cmdSave() int { - switch state := status(B2D.VM); state { - case vmVBMNotFound: - logf("Failed to locate VirtualBox management utility %q", B2D.VBM) - return 2 - case vmUnregistered: - logf("VM %q is not registered.", B2D.VM) - return 1 - case vmPaused: // resume from paused before saving - if exitcode := cmdUp(); exitcode != 0 { - return exitcode - } - case vmRunning: - break - default: - logf("VM %q is not running.", B2D.VM) - return 0 - } - - logf("Suspending VM %q", B2D.VM) - if err := vbm("controlvm", B2D.VM, "savestate"); err != nil { - logf("Failed to suspend VM %q: %s", B2D.VM, err) - return 1 - } - return 0 -} - -// Pause the VM. -func cmdPause() int { - switch state := status(B2D.VM); state { - case vmVBMNotFound: - logf("Failed to locate VirtualBox management utility %q", B2D.VBM) + m, err := vbx.GetMachine(B2D.VM) + if err != nil { + logf("Failed to get machine %q: %s", B2D.VM, err) return 2 - case vmUnregistered: - logf("VM %q is not registered.", B2D.VM) - return 1 - case vmRunning: - break - default: - logf("VM %q is not running.", B2D.VM) - return 0 } - - if err := vbm("controlvm", B2D.VM, "pause"); err != nil { - logf("Failed to pause VM %q: %s", B2D.VM, err) + if err := m.Save(); err != nil { + logf("Failed to save machine %q: %s", B2D.VM, err) return 1 } return 0 @@ -334,58 +190,28 @@ func cmdPause() int { // Gracefully stop the VM by sending ACPI shutdown signal. func cmdStop() int { - switch state := status(B2D.VM); state { - case vmVBMNotFound: - logf("Failed to locate VirtualBox management utility %q", B2D.VBM) + m, err := vbx.GetMachine(B2D.VM) + if err != nil { + logf("Failed to get machine %q: %s", B2D.VM, err) return 2 - case vmUnregistered: - logf("VM %q is not registered.", B2D.VM) - return 1 - case vmPaused, vmSaved: // resume before stopping - if exitcode := cmdUp(); exitcode != 0 { - return exitcode - } - case vmRunning: - break - default: - logf("VM %q is not running.", B2D.VM) - return 0 } - - logf("Shutting down VM %q...", B2D.VM) - if err := vbm("controlvm", B2D.VM, "acpipowerbutton"); err != nil { - logf("Failed to shutdown VM %q: %s", B2D.VM, err) + if err := m.Stop(); err != nil { + logf("Failed to stop machine %q: %s", B2D.VM, err) return 1 } - for status(B2D.VM) == vmRunning { - time.Sleep(1 * time.Second) - } return 0 } // Forcefully power off the VM (equivalent to unplug power). Might corrupt disk // image. func cmdPoweroff() int { - switch state := status(B2D.VM); state { - case vmVBMNotFound: - logf("Failed to locate VirtualBox management utility %q", B2D.VBM) + m, err := vbx.GetMachine(B2D.VM) + if err != nil { + logf("Failed to get machine %q: %s", B2D.VM, err) return 2 - case vmUnregistered: - logf("VM %q is not registered.", B2D.VM) - return 1 - case vmRunning, vmPaused: - break - case vmSaved: - if exitcode := cmdUp(); exitcode != 0 { - return exitcode - } - default: - logf("VM %q is not running.", B2D.VM) - return 0 } - - if err := vbm("controlvm", B2D.VM, "poweroff"); err != nil { - logf("Failed to poweroff VM %q: %s", B2D.VM, err) + if err := m.Poweroff(); err != nil { + logf("Failed to poweroff machine %q: %s", B2D.VM, err) return 1 } return 0 @@ -393,45 +219,27 @@ func cmdPoweroff() int { // Gracefully stop and then start the VM. func cmdRestart() int { - switch state := status(B2D.VM); state { - case vmPaused, vmSaved: - if exitcode := cmdUp(); exitcode != 0 { - return exitcode - } - fallthrough // important! - case vmRunning: - if exitcode := cmdStop(); exitcode != 0 { - return exitcode - } - default: - logf("Cannot restart VM %q from state %s", B2D.VM, state) + m, err := vbx.GetMachine(B2D.VM) + if err != nil { + logf("Failed to get machine %q: %s", B2D.VM, err) + return 2 + } + if err := m.Restart(); err != nil { + logf("Failed to restart machine %q: %s", B2D.VM, err) return 1 } - return cmdUp() + return 0 } // Forcefully reset (equivalent to cold boot) the VM. Might corrupt disk image. func cmdReset() int { - switch state := status(B2D.VM); state { - case vmVBMNotFound: - logf("Failed to locate VirtualBox management utility %q", B2D.VBM) + m, err := vbx.GetMachine(B2D.VM) + if err != nil { + logf("Failed to get machine %q: %s", B2D.VM, err) return 2 - case vmUnregistered: - logf("VM %q is not registered.", B2D.VM) - return 1 - case vmPaused, vmSaved: - if exitcode := cmdUp(); exitcode != 0 { - return exitcode - } - case vmRunning: - break - default: - logf("VM %q is not running.", B2D.VM) - return 0 } - - if err := vbm("controlvm", B2D.VM, "reset"); err != nil { - logf("Failed to reset VM %q: %s", B2D.VM, err) + if err := m.Reset(); err != nil { + logf("Failed to reset machine %q: %s", B2D.VM, err) return 1 } return 0 @@ -439,21 +247,17 @@ func cmdReset() int { // Delete the VM and associated disk image. func cmdDelete() int { - switch state := status(B2D.VM); state { - case vmVBMNotFound: - logf("Failed to locate VirtualBox management utility %q", B2D.VBM) - return 2 - case vmUnregistered: - logf("VM %q is not registered.", B2D.VM) - return 1 - case vmRunning, vmPaused: - if exitcode := cmdPoweroff(); exitcode != 0 { - return exitcode + m, err := vbx.GetMachine(B2D.VM) + if err != nil { + if err == vbx.ErrMachineNotExist { + logf("Machine %q does not exist.", B2D.VM) + return 0 } + logf("Failed to get machine %q: %s", B2D.VM, err) + return 2 } - - if err := vbm("unregistervm", "--delete", B2D.VM); err != nil { - logf("Failed to delete VM %q: %s", B2D.VM, err) + if err := m.Delete(); err != nil { + logf("Failed to delete machine %q: %s", B2D.VM, err) return 1 } return 0 @@ -461,17 +265,13 @@ func cmdDelete() int { // Show detailed info of the VM. func cmdInfo() int { - switch state := status(B2D.VM); state { - case vmVBMNotFound: - logf("Failed to locate VirtualBox management utility %q", B2D.VBM) + m, err := vbx.GetMachine(B2D.VM) + if err != nil { + logf("Failed to get machine %q: %s", B2D.VM, err) return 2 - case vmUnregistered: - logf("%q does not exist", B2D.VM) - return 1 } - - if err := vbm("showvminfo", B2D.VM); err != nil { - logf("Failed to show info of VM %q: %s", B2D.VM, err) + if err := json.NewEncoder(os.Stdout).Encode(m); err != nil { + logf("Failed to encode machine %q info: %s", B2D.VM, err) return 1 } return 0 @@ -479,31 +279,24 @@ func cmdInfo() int { // Show the current state of the VM. func cmdStatus() int { - switch state := status(B2D.VM); state { - case vmVBMNotFound: - logf("Failed to locate VirtualBox management utility %q", B2D.VBM) + m, err := vbx.GetMachine(B2D.VM) + if err != nil { + logf("Failed to get machine %q: %s", B2D.VM, err) return 2 - case vmUnregistered: - logf("VM %q does not exist", B2D.VM) - return 1 - default: - logf(string(state)) - return 0 } + fmt.Println(m.State) + return 0 } // Call the external SSH command to login into boot2docker VM. func cmdSSH() int { - switch state := status(B2D.VM); state { - case vmVBMNotFound: - logf("Failed to locate VirtualBox management utility %q", B2D.VBM) + m, err := vbx.GetMachine(B2D.VM) + if err != nil { + logf("Failed to get machine %q: %s", B2D.VM, err) return 2 - case vmUnregistered: - logf("VM %q is not registered.", B2D.VM) - return 1 - case vmRunning: - break - default: + } + + if m.State != vbx.Running { logf("VM %q is not running.", B2D.VM) return 1 } diff --git a/config.go b/config.go index a81ee26..85fc65e 100644 --- a/config.go +++ b/config.go @@ -1,15 +1,17 @@ package main import ( + "bufio" "fmt" + "net" "os" "path/filepath" + "regexp" "runtime" - "strconv" + "strings" - // keep 3rd-party imports separate from stdlib with an empty line + vbx "github.com/boot2docker/boot2docker-cli/virtualbox" flag "github.com/ogier/pflag" - ini "github.com/vaughan0/go-ini" ) // boot2docker config. @@ -17,8 +19,11 @@ var B2D struct { // NOTE: separate sections with blank lines so gofmt doesn't change // indentation all the time. + // Gereral flags. + Verbose bool + VBM string + // basic config - VBM string // VirtualBox management utility SSH string // SSH client executable VM string // virtual machine name Dir string // boot2docker directory @@ -31,17 +36,17 @@ var B2D struct { DockerPort uint16 // host Docker port (forward to port 4243 in VM) // host-only network - HostIP string - DHCPIP string - NetworkMask string - LowerIPAddress string - UpperIPAddress string - DHCPEnabled string + HostIP net.IP + DHCPIP net.IP + NetMask net.IPMask + LowerIP net.IP + UpperIP net.IP + DHCPEnabled bool } -// General flags. var ( - verboseFlag = flag.BoolP("verbose", "v", false, "display verbose command invocations.") + // Pattern to parse a key=value line in config profile. + reFlagLine = regexp.MustCompile(`^\s*(\w+)\s*=\s*([^#;]+)`) ) func getCfgDir(name string) (string, error) { @@ -73,118 +78,120 @@ func getCfgDir(name string) (string, error) { } // Read configuration from both profile and flags. Flags override profile. -func config() error { - var err error - if B2D.Dir, err = getCfgDir(".boot2docker"); err != nil { - return fmt.Errorf("failed to get current directory: %s", err) +func config() (*flag.FlagSet, error) { + dir, err := getCfgDir(".boot2docker") + if err != nil { + return nil, fmt.Errorf("failed to get boot2docker directory: %s", err) } filename := os.Getenv("BOOT2DOCKER_PROFILE") if filename == "" { - filename = filepath.Join(B2D.Dir, "profile") - } - profile, err := getProfile(filename) - if err != nil && !os.IsNotExist(err) { // undefined/empty profile works - return err + filename = filepath.Join(dir, "profile") } - if p := os.Getenv("VBOX_INSTALL_PATH"); p != "" && runtime.GOOS == "windows" { - B2D.VBM = profile.Get("", "vbm", filepath.Join(p, "VBoxManage.exe")) - } else { - B2D.VBM = profile.Get("", "vbm", "VBoxManage") + profileArgs, err := readProfile(filename) + if err != nil && !os.IsNotExist(err) { // undefined/empty profile works + return nil, err } - B2D.SSH = profile.Get("", "ssh", "ssh") - B2D.VM = profile.Get("", "vm", "boot2docker-vm") - B2D.ISO = profile.Get("", "iso", filepath.Join(B2D.Dir, "boot2docker.iso")) + flags := flag.NewFlagSet(os.Args[0], flag.ContinueOnError) + flags.Usage = func() { usageLong(flags) } - if diskSize, err := strconv.ParseUint(profile.Get("", "disksize", "20000"), 10, 32); err != nil { - return fmt.Errorf("invalid disk image size: %s", err) - } else { - B2D.DiskSize = uint(diskSize) + flags.StringVar(&B2D.VM, "vm", "boot2docker-vm", "virtual machine name.") + flags.StringVarP(&B2D.Dir, "dir", "d", dir, "boot2docker config directory.") + flags.StringVar(&B2D.ISO, "iso", filepath.Join(dir, "boot2docker.iso"), "path to boot2docker ISO image.") + vbm := "VBoxManage" + if p := os.Getenv("VBOX_INSTALL_PATH"); p != "" && runtime.GOOS == "windows" { + vbm = filepath.Join(p, "VBoxManage.exe") } - - if memory, err := strconv.ParseUint(profile.Get("", "memory", "1024"), 10, 32); err != nil { - return fmt.Errorf("invalid memory size: %s", err) - } else { - B2D.Memory = uint(memory) + flags.StringVar(&B2D.VBM, "vbm", vbm, "path to VirtualBox management utility.") + flags.BoolVarP(&B2D.Verbose, "verbose", "v", false, "display verbose command invocations.") + flags.StringVar(&B2D.SSH, "ssh", "ssh", "path to SSH client utility.") + flags.UintVarP(&B2D.DiskSize, "disksize", "s", 20000, "boot2docker disk image size (in MB).") + flags.UintVarP(&B2D.Memory, "memory", "m", 1024, "virtual machine memory size (in MB).") + flags.Uint16Var(&B2D.SSHPort, "sshport", 2022, "host SSH port (forward to port 22 in VM).") + flags.Uint16Var(&B2D.DockerPort, "dockerport", 4243, "host Docker port (forward to port 4243 in VM).") + flags.IPVar(&B2D.HostIP, "hostip", net.ParseIP("192.168.59.3"), "VirtualBox host-only network IP address.") + flags.IPMaskVar(&B2D.NetMask, "netmask", flag.ParseIPv4Mask("255.255.255.0"), "VirtualBox host-only network mask.") + flags.BoolVar(&B2D.DHCPEnabled, "dhcp", true, "enable VirtualBox host-only network DHCP.") + flags.IPVar(&B2D.DHCPIP, "dhcpip", net.ParseIP("192.168.59.99"), "VirtualBox host-only network DHCP server address.") + flags.IPVar(&B2D.LowerIP, "lowerip", net.ParseIP("192.168.59.103"), "VirtualBox host-only network DHCP lower bound.") + flags.IPVar(&B2D.UpperIP, "upperip", net.ParseIP("192.168.59.254"), "VirtualBox host-only network DHCP upper bound.") + + // Command-line overrides profile config. + if err := flags.Parse(append(profileArgs, os.Args[1:]...)); err != nil { + return nil, err } - if sshPort, err := strconv.ParseUint(profile.Get("", "sshport", "2022"), 10, 16); err != nil { - return fmt.Errorf("invalid SSH port: %s", err) - } else { - B2D.SSHPort = uint16(sshPort) - } - - if dockerPort, err := strconv.ParseUint(profile.Get("", "dockerport", "4243"), 10, 16); err != nil { - return fmt.Errorf("invalid DockerPort: %s", err) - } else { - B2D.DockerPort = uint16(dockerPort) - } - - // Host only networking settings - B2D.HostIP = profile.Get("", "hostiP", "192.168.59.3") - B2D.DHCPIP = profile.Get("", "dhcpip", "192.168.59.99") - B2D.NetworkMask = profile.Get("", "netmask", "255.255.255.0") - B2D.LowerIPAddress = profile.Get("", "lowerip", "192.168.59.103") - B2D.UpperIPAddress = profile.Get("", "upperip", "192.168.59.254") - B2D.DHCPEnabled = profile.Get("", "dhcp", "Yes") - - // Commandline flags override profile settings. - flag.StringVar(&B2D.VBM, "vbm", B2D.VBM, "Path to VirtualBox management utility") - flag.StringVar(&B2D.SSH, "ssh", B2D.SSH, "Path to SSH client utility") - flag.StringVarP(&B2D.Dir, "dir", "d", B2D.Dir, "boot2docker config directory") - flag.StringVar(&B2D.ISO, "iso", B2D.ISO, "Path to boot2docker ISO image") - flag.UintVarP(&B2D.DiskSize, "disksize", "s", B2D.DiskSize, "boot2docker disk image size (in MB)") - flag.UintVarP(&B2D.Memory, "memory", "m", B2D.Memory, "Virtual machine memory size (in MB)") - flag.Var(newUint16Value(B2D.SSHPort, &B2D.SSHPort), "sshport", "Host SSH port (forward to port 22 in VM)") - flag.Var(newUint16Value(B2D.DockerPort, &B2D.DockerPort), "dockerport", "Host Docker port (forward to port 4243 in VM)") - flag.StringVar(&B2D.HostIP, "hostip", B2D.HostIP, "VirtualBox host-only network IP address") - flag.StringVar(&B2D.NetworkMask, "netmask", B2D.NetworkMask, "VirtualBox host-only network mask") - flag.StringVar(&B2D.DHCPEnabled, "dhcp", B2D.DHCPEnabled, "Enable VirtualBox host-only network DHCP") - flag.StringVar(&B2D.DHCPIP, "dhcpip", B2D.DHCPIP, "VirtualBox host-only network DHCP server address") - flag.StringVar(&B2D.LowerIPAddress, "lowerip", B2D.LowerIPAddress, "VirtualBox host-only network DHCP lower bound") - flag.StringVar(&B2D.UpperIPAddress, "upperip", B2D.UpperIPAddress, "VirtualBox host-only network DHCP upper bound") - - flag.Parse() - - // Name of VM is the second argument. - if vm := flag.Arg(1); vm != "" { + // Name of VM is the second argument. Override the value set in flag. + if vm := flags.Arg(1); vm != "" { B2D.VM = vm } - return nil -} -// boot2docker configuration profile. -type Profile struct { - ini.File + vbx.Verbose = B2D.Verbose + vbx.VBM = B2D.VBM + return flags, nil } -func getProfile(filename string) (*Profile, error) { - f, err := ini.LoadFile(filename) - return &Profile{f}, err -} +// Read boot2docker configuration profile into string slice. Expanding +// $ENVVARS in the values field. +func readProfile(filename string) ([]string, error) { + f, err := os.Open(filename) + if err != nil { + return nil, err + } + defer f.Close() + + args := []string{} + s := bufio.NewScanner(f) + ln := 0 + for s.Scan() { + ln++ + line := strings.TrimSpace(s.Text()) + if strings.HasPrefix(line, "#") || strings.HasPrefix(line, ";") { + // Ignore comment lines starting with # or ; + continue + } + res := reFlagLine.FindStringSubmatch(line) + if res == nil { + return nil, fmt.Errorf("failed to parse profile line %d: %q", ln, line) + } + args = append(args, fmt.Sprintf("--%v=%v", res[1], os.ExpandEnv(res[2]))) + } -func (f *Profile) Get(section, key, fallback string) string { - if val, ok := f.File.Get(section, key); ok { - return os.ExpandEnv(val) + if err := s.Err(); err != nil { + return nil, err } - return fallback + return args, nil } -// The missing flag.Uint16Var value type. -type uint16Value uint16 +func usageShort() { + errf("Usage: %s [] {help|init|up|ssh|save|down|poweroff|reset|restart|status|info|delete|download|version} []\n", os.Args[0]) -func newUint16Value(val uint16, p *uint16) *uint16Value { - *p = val - return (*uint16Value)(p) } -func (i *uint16Value) String() string { return fmt.Sprintf("%d", *i) } -func (i *uint16Value) Set(s string) error { - v, err := strconv.ParseUint(s, 10, 16) - *i = uint16Value(v) - return err -} -func (i *uint16Value) Get() interface{} { - return uint16(*i) + +func usageLong(flags *flag.FlagSet) { + // NOTE: the help message uses spaces, not tabs for indentation! + errf(`Usage: %s [] [] + +boot2docker management utility. + +Commands: + init [] Create a new boot2docker VM. + up|start|boot [] Start VM from any states. + ssh Login to VM via SSH. + save|suspend [] Suspend VM and save state to disk. + down|stop|halt [] Gracefully shutdown the VM. + restart [] Gracefully reboot the VM. + poweroff [] Forcefully power off the VM (might corrupt disk image). + reset [] Forcefully power cycle the VM (might corrupt disk image). + delete [] Delete boot2docker VM and its disk image. + info [] Display detailed information of VM. + status [] Display current state of VM. + download Download boot2docker ISO image. + version Display version information. + +Options: +`, os.Args[0]) + flags.PrintDefaults() } diff --git a/main.go b/main.go index df0f2cd..bae2c59 100644 --- a/main.go +++ b/main.go @@ -1,12 +1,6 @@ -// This is the boot2docker management utilty. package main -import ( - "os" - - // keep 3rd-party imports separate from stdlib with an empty line - flag "github.com/ogier/pflag" -) +import "os" // The following vars will be injected during the build process. var ( @@ -19,20 +13,19 @@ func main() { // any deferred cleanup statements. It might cause unintended effects. To // be safe, we wrap the program in run() and only os.Exit() outside the // wrapper. Be careful not to indirectly trigger os.Exit() in the program, - // notably via log.Fatal(). + // notably via log.Fatal() and on flag.Parse() where the default behavior + // is ExitOnError. os.Exit(run()) } // Run the program and return exit code. func run() int { - flag.Usage = usageLong // make "-h" work similarly to "help" - - if err := config(); err != nil { - errf("%s\n", err) + flags, err := config() + if err != nil { return 1 } - switch cmd := flag.Arg(0); cmd { + switch cmd := flags.Arg(0); cmd { case "download": return cmdDownload() case "init": @@ -61,7 +54,7 @@ func run() int { outf("Client version: %s\nGit commit: %s\n", Version, GitSHA) return 0 case "help": - flag.Usage() + flags.Usage() return 0 case "": usageShort() @@ -72,34 +65,3 @@ func run() int { return 1 } } - -func usageShort() { - errf("Usage: %s [] {help|init|up|ssh|save|down|poweroff|reset|restart|status|info|delete|download|version} []\n", os.Args[0]) - -} - -func usageLong() { - // NOTE: the help message uses spaces, not tabs for indentation! - errf(`boot2docker management utility. - -Usage: %s [] [] - -Commands: - init [] Create a new boot2docker VM. - up|start|boot [] Start VM from any states. - ssh Login to VM via SSH. - save|suspend [] Suspend VM and save state to disk. - down|stop|halt [] Gracefully shutdown the VM. - restart [] Gracefully reboot the VM. - poweroff [] Forcefully power off the VM (might corrupt disk image). - reset [] Forcefully power cycle the VM (might corrupt disk image). - delete [] Delete boot2docker VM and its disk image. - info [] Display detailed information of VM. - status [] Display current state of VM. - download Download boot2docker ISO image. - version Display version information. - -Options: -`, os.Args[0]) - flag.PrintDefaults() -} diff --git a/util.go b/util.go index 8ba574e..65e7696 100644 --- a/util.go +++ b/util.go @@ -8,7 +8,10 @@ import ( "net" "net/http" "os" + "os/exec" "path/filepath" + "strings" + "time" ) // fmt.Printf to stdout. Convention is to outf info intended for scripting. @@ -26,17 +29,29 @@ func logf(fmt string, v ...interface{}) { log.Printf(fmt, v...) } -// Check if the connection to tcp://addr is readable. -func read(addr string) error { - conn, err := net.Dial("tcp", addr) - if err != nil { - return err - } - defer conn.Close() - if _, err = conn.Read(make([]byte, 1)); err != nil { - return err +// Try if addr tcp://addr is readable for n times at wait interval. +func read(addr string, n int, wait time.Duration) error { + var lastErr error + for i := 0; i < n; i++ { + if B2D.Verbose { + logf("Connecting to tcp://%v (attempt #%d)", addr, i) + } + conn, err := net.DialTimeout("tcp", addr, 1*time.Second) + if err != nil { + lastErr = err + time.Sleep(wait) + continue + } + defer conn.Close() + conn.SetDeadline(time.Now().Add(1 * time.Second)) + if _, err = conn.Read(make([]byte, 1)); err != nil { + lastErr = err + time.Sleep(wait) + continue + } + return nil } - return nil + return lastErr } // Check if an addr can be successfully connected. @@ -100,3 +115,14 @@ func getLatestReleaseName(url string) (string, error) { } return t[0].TagName, nil } + +// Convenient function to exec a command. +func cmd(name string, args ...string) error { + cmd := exec.Command(name, args...) + if B2D.Verbose { + logf("executing: %v %v", name, strings.Join(args, " ")) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + } + return cmd.Run() +} diff --git a/vbm.go b/vbm.go index b66559c..794b06f 100644 --- a/vbm.go +++ b/vbm.go @@ -1,173 +1,69 @@ package main import ( - "fmt" - "io" + "bytes" "os" - "os/exec" "path/filepath" - "regexp" - "strings" -) - -// Convenient function to exec a command. -func cmd(name string, args ...string) error { - if *verboseFlag { - logf("executing: %v %v", name, strings.Join(args, " ")) - } - cmd := exec.Command(name, args...) - cmd.Stdin = os.Stdin - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - return cmd.Run() -} -// Convenient function to launch VBoxManage. -func vbm(args ...string) error { - return cmd(B2D.VBM, args...) -} - -func vbmOut(args ...string) ([]byte, error) { - if *verboseFlag { - logf("executing: %v %v", B2D.VBM, strings.Join(args, " ")) - } - return exec.Command(B2D.VBM, args...).Output() -} + vbx "github.com/boot2docker/boot2docker-cli/virtualbox" +) // TODO: delete the hostonlyif and dhcpserver when we delete the vm! (need to // reference count to make sure there are no other vms relying on them) // Get or create the hostonly network interface func getHostOnlyNetworkInterface() (string, error) { - // Check if the interface exists. - args := []string{"list", "hostonlyifs"} - out, err := vbmOut(args...) + // Check if the interface/dhcp exists. + nets, err := vbx.HostonlyNets() if err != nil { return "", err } - lists := regexp.MustCompile(`(?m)^(Name|IPAddress|VBoxNetworkName):\s+(.+?)\r?$`).FindAllSubmatch(out, -1) - var ifname string - index := 0 - - for ifname == "" && len(lists) > index { - if string(lists[index+1][2]) == B2D.HostIP { - //test to see that the dhcpserver is the same too - args = []string{"list", "dhcpservers"} - out, err := vbmOut(args...) - if err != nil { - return "", err - } - dhcp := regexp.MustCompile(`(?m)^(NetworkName|IP|NetworkMask|lowerIPAddress|upperIPAddress|Enabled):\s+(.+?)\r?$`).FindAllSubmatch(out, -1) - i := 0 - for ifname == "" && len(dhcp) > i { - info := map[string]string{} - for id := 0; id < 6; id++ { - info[string(dhcp[i][1])] = string(dhcp[i][2]) - i++ - } + dhcps, err := vbx.DHCPs() + if err != nil { + return "", err + } - if info["NetworkName"] == string(lists[index+2][2]) && - info["IP"] == B2D.DHCPIP && - info["NetworkMask"] == B2D.NetworkMask && - info["lowerIPAddress"] == B2D.LowerIPAddress && - info["upperIPAddress"] == B2D.UpperIPAddress && - info["Enabled"] == B2D.DHCPEnabled { - ifname = string(lists[index][2]) - logf("Reusing hostonly network interface %s", ifname) + for _, n := range nets { + if dhcp, ok := dhcps[n.NetworkName]; ok { + if dhcp.IPv4.IP.Equal(B2D.DHCPIP) && + dhcp.IPv4.Mask.String() == B2D.NetMask.String() && + dhcp.LowerIP.Equal(B2D.LowerIP) && + dhcp.UpperIP.Equal(B2D.UpperIP) && + dhcp.Enabled == B2D.DHCPEnabled { + if B2D.Verbose { + logf("Reusing existing host-only network interface %q", n.Name) } + return n.Name, nil } } - index = index + 3 } - if ifname == "" { - //create it all fresh - logf("Creating a new hostonly network interface") - out, err = exec.Command(B2D.VBM, "hostonlyif", "create").Output() - if err != nil { - return "", err - } - groups := regexp.MustCompile(`(?m)^Interface '(.+)' was successfully created`).FindSubmatch(out) - if len(groups) < 2 { - return "", err - } - ifname = string(groups[1]) - args = []string{ - "dhcpserver", "add", - "--ifname", ifname, - "--ip", B2D.DHCPIP, - "--netmask", B2D.NetworkMask, - "--lowerip", B2D.LowerIPAddress, - "--upperip", B2D.UpperIPAddress, - "--enable", - } - out, err = vbmOut(args...) - if err != nil { - return "", err - } - args = []string{ - "hostonlyif", "ipconfig", ifname, - "--ip", B2D.HostIP, - "--netmask", B2D.NetworkMask, - } - out, err = vbmOut(args...) - if err != nil { - return "", err - } - } - return ifname, nil -} - -// Get the state of a VM. -func status(vm string) vmState { - // Check if the VM exists. - args := []string{"list", "vms"} - out, err := vbmOut(args...) - if err != nil { - if ee, ok := err.(*exec.Error); ok && ee == exec.ErrNotFound { - return vmVBMNotFound - } - return vmUnknown + // No existing host-only interface found. Create a new one. + if B2D.Verbose { + logf("Creating a new host-only network interface...") } - found, err := regexp.Match(fmt.Sprintf(`(?m)^"%s"`, regexp.QuoteMeta(vm)), out) + hostonlyNet, err := vbx.CreateHostonlyNet() if err != nil { - return vmUnknown - } - if !found { - return vmUnregistered - } - - if out, err = exec.Command(B2D.VBM, "showvminfo", vm, "--machinereadable").Output(); err != nil { - if ee, ok := err.(*exec.Error); ok && ee == exec.ErrNotFound { - return vmVBMNotFound - } - return vmUnknown - } - groups := regexp.MustCompile(`(?m)^VMState="(\w+)"\r?$`).FindSubmatch(out) - if len(groups) < 2 { - return vmUnknown + return "", err } - switch state := vmState(groups[1]); state { - case vmRunning, vmPaused, vmSaved, vmPoweroff, vmAborted: - return state - default: - return vmUnknown + hostonlyNet.IPv4.IP = B2D.HostIP + hostonlyNet.IPv4.Mask = B2D.NetMask + if err := hostonlyNet.Config(); err != nil { + return "", err } -} -// Get the VirtualBox base folder of the VM. -func basefolder(vm string) string { - args := []string{"showvminfo", vm, "--machinereadable"} - out, err := vbmOut(args...) - if err != nil { - return "" - } - groups := regexp.MustCompile(`(?m)^CfgFile="(.+)"\r?$`).FindSubmatch(out) - if len(groups) < 2 { - return "" + // Create and add a DHCP server to the host-only network + dhcp := vbx.DHCP{} + dhcp.IPv4.IP = B2D.DHCPIP + dhcp.IPv4.Mask = B2D.NetMask + dhcp.LowerIP = B2D.LowerIP + dhcp.UpperIP = B2D.UpperIP + dhcp.Enabled = true + if err := vbx.AddHostonlyDHCP(hostonlyNet.Name, dhcp); err != nil { + return "", err } - return filepath.Dir(string(groups[1])) + return hostonlyNet.Name, nil } // Make a boot2docker VM disk image with the given size (in MB). @@ -176,61 +72,8 @@ func makeDiskImage(dest string, size uint) error { if err := os.MkdirAll(filepath.Dir(dest), 0755); err != nil { return err } - - // Convert a raw image from stdin to the dest VMDK image. - sizeBytes := int64(size) * 1024 * 1024 // usually won't fit in 32-bit int - args := []string{"convertfromraw", "stdin", dest, - fmt.Sprintf("%d", sizeBytes), "--format", "VMDK", - } - if *verboseFlag { - logf("executing: %v %v", B2D.VBM, strings.Join(args, " ")) - } - cmd := exec.Command(B2D.VBM, args...) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - stdin, err := cmd.StdinPipe() - if err != nil { - return err - } - if err := cmd.Start(); err != nil { - return err - } - // Fill in the magic string so boot2docker VM will detect this and format // the disk upon first boot. - magic := []byte("boot2docker, please format-me") - if _, err := stdin.Write(magic); err != nil { - return err - } - // The total number of bytes written to stdin must match sizeBytes, or - // VBoxManage.exe on Windows will fail. - if err := zeroFill(stdin, sizeBytes-int64(len(magic))); err != nil { - return err - } - // cmd won't exit until the stdin is closed. - if err := stdin.Close(); err != nil { - return err - } - - return cmd.Wait() -} - -// Write n zero bytes into w. -func zeroFill(w io.Writer, n int64) error { - const blocksize = 32 * 1024 - zeros := make([]byte, blocksize) - var k int - var err error - for n > 0 { - if n > blocksize { - k, err = w.Write(zeros) - } else { - k, err = w.Write(zeros[:n]) - } - if err != nil { - return err - } - n -= int64(k) - } - return nil + raw := bytes.NewReader([]byte("boot2docker, please format-me")) + return vbx.MakeDiskImage(dest, size, raw) } diff --git a/virtualbox/README.md b/virtualbox/README.md new file mode 100644 index 0000000..0fa8913 --- /dev/null +++ b/virtualbox/README.md @@ -0,0 +1,6 @@ +# go-virtualbox + +This is a wrapper package for Golang to interact with VirtualBox. The API is +experimental at the moment and you should expect frequent changes. + +API doc at http://godoc.org/github.com/riobard/go-virtualbox diff --git a/virtualbox/dhcp.go b/virtualbox/dhcp.go new file mode 100644 index 0000000..c2f2d47 --- /dev/null +++ b/virtualbox/dhcp.go @@ -0,0 +1,83 @@ +package virtualbox + +import ( + "bufio" + "net" + "strings" +) + +// DHCP server info. +type DHCP struct { + NetworkName string + IPv4 net.IPNet + LowerIP net.IP + UpperIP net.IP + Enabled bool +} + +func addDHCP(kind, name string, d DHCP) error { + args := []string{"dhcpserver", "add", + kind, name, + "--ip", d.IPv4.IP.String(), + "--netmask", net.IP(d.IPv4.Mask).String(), + "--lowerip", d.LowerIP.String(), + "--upperip", d.UpperIP.String(), + } + if d.Enabled { + args = append(args, "--enable") + } else { + args = append(args, "--disable") + } + return vbm(args...) +} + +// AddInternalDHCP adds a DHCP server to an internal network. +func AddInternalDHCP(netname string, d DHCP) error { + return addDHCP("--netname", netname, d) +} + +// AddHostonlyDHCP adds a DHCP server to a host-only network. +func AddHostonlyDHCP(ifname string, d DHCP) error { + return addDHCP("--ifname", ifname, d) +} + +// DHCPs gets all DHCP server settings in a map keyed by DHCP.NetworkName. +func DHCPs() (map[string]*DHCP, error) { + out, err := vbmOut("list", "dhcpservers") + if err != nil { + return nil, err + } + s := bufio.NewScanner(strings.NewReader(out)) + m := map[string]*DHCP{} + dhcp := &DHCP{} + for s.Scan() { + line := s.Text() + if line == "" { + m[dhcp.NetworkName] = dhcp + dhcp = &DHCP{} + continue + } + res := reColonLine.FindStringSubmatch(line) + if res == nil { + continue + } + switch key, val := res[1], res[2]; key { + case "NetworkName": + dhcp.NetworkName = val + case "IP": + dhcp.IPv4.IP = net.ParseIP(val) + case "upperIPAddress": + dhcp.UpperIP = net.ParseIP(val) + case "lowerIPAddress": + dhcp.LowerIP = net.ParseIP(val) + case "NetworkMask": + dhcp.IPv4.Mask = ParseIPv4Mask(val) + case "Enabled": + dhcp.Enabled = (val == "Yes") + } + } + if err := s.Err(); err != nil { + return nil, err + } + return m, nil +} diff --git a/virtualbox/dhcp_test.go b/virtualbox/dhcp_test.go new file mode 100644 index 0000000..72afa71 --- /dev/null +++ b/virtualbox/dhcp_test.go @@ -0,0 +1,14 @@ +package virtualbox + +import "testing" + +func TestDHCPs(t *testing.T) { + m, err := DHCPs() + if err != nil { + t.Fatal(err) + } + + for _, dhcp := range m { + t.Logf("%+v", dhcp) + } +} diff --git a/virtualbox/disk.go b/virtualbox/disk.go new file mode 100644 index 0000000..988dae8 --- /dev/null +++ b/virtualbox/disk.go @@ -0,0 +1,70 @@ +package virtualbox + +import ( + "fmt" + "io" + "os" + "os/exec" +) + +// MakeDiskImage makes a disk image at dest with the given size in MB. If r is +// not nil, it will be read as a raw disk image to convert from. +func MakeDiskImage(dest string, size uint, r io.Reader) error { + // Convert a raw image from stdin to the dest VMDK image. + sizeBytes := int64(size) << 20 // usually won't fit in 32-bit int (max 2GB) + cmd := exec.Command(VBM, "convertfromraw", "stdin", dest, + fmt.Sprintf("%d", sizeBytes), "--format", "VMDK") + + if Verbose { + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + } + + stdin, err := cmd.StdinPipe() + if err != nil { + return err + } + if err := cmd.Start(); err != nil { + return err + } + + n, err := io.Copy(stdin, r) + if err != nil { + return err + } + + // The total number of bytes written to stdin must match sizeBytes, or + // VBoxManage.exe on Windows will fail. Fill remaining with zeros. + if left := sizeBytes - n; left > 0 { + if err := ZeroFill(stdin, left); err != nil { + return err + } + } + + // cmd won't exit until the stdin is closed. + if err := stdin.Close(); err != nil { + return err + } + + return cmd.Wait() +} + +// ZeroFill writes n zero bytes into w. +func ZeroFill(w io.Writer, n int64) error { + const blocksize = 32 << 10 + zeros := make([]byte, blocksize) + var k int + var err error + for n > 0 { + if n > blocksize { + k, err = w.Write(zeros) + } else { + k, err = w.Write(zeros[:n]) + } + if err != nil { + return err + } + n -= int64(k) + } + return nil +} diff --git a/virtualbox/doc.go b/virtualbox/doc.go new file mode 100644 index 0000000..5528144 --- /dev/null +++ b/virtualbox/doc.go @@ -0,0 +1,41 @@ +/* +Package virtualbox implements wrappers to interact with VirtualBox. + +VirtualBox Machine State Transition + +A VirtualBox machine can be in one of the following states: + + poweroff: The VM is powered off and no previous running state saved. + running: The VM is running. + paused: The VM is paused, but its state is not saved to disk. If you quit VirtualBox, the state will be lost. + saved: The VM is powered off, and the previous state is saved on disk. + aborted: The VM process crashed. This should happen very rarely. + +VBoxManage supports the following transitions between states: + + startvm : poweroff|saved --> running + controlvm pause: running --> paused + controlvm resume: paused --> running + controlvm savestate: running -> saved + controlvm acpipowerbutton: running --> poweroff + controlvm poweroff: running --> poweroff (unsafe) + controlvm reset: running --> poweroff --> running (unsafe) + +Poweroff and reset are unsafe because they will lose state and might corrupt +the disk image. + +To make things simpler, the following transitions are used instead: + + start: poweroff|saved|paused|aborted --> running + stop: [paused|saved -->] running --> poweroff + save: [paused -->] running --> saved + restart: [paused|saved -->] running --> poweroff --> running + poweroff: [paused|saved -->] running --> poweroff (unsafe) + reset: [paused|saved -->] running --> poweroff --> running (unsafe) + +The takeaway is we try our best to transit the virtual machine into the state +you want it to be, and you only need to watch out for the potentially unsafe +poweroff and reset. + +*/ +package virtualbox diff --git a/virtualbox/extra.go b/virtualbox/extra.go new file mode 100644 index 0000000..2a33f43 --- /dev/null +++ b/virtualbox/extra.go @@ -0,0 +1,11 @@ +package virtualbox + +// SetExtra sets extra data. Name could be "global"|| +func SetExtra(name, key, val string) error { + return vbm("setextradata", name, key, val) +} + +// DelExtraData deletes extra data. Name could be "global"|| +func DelExtra(name, key string) error { + return vbm("setextradata", name, key) +} diff --git a/virtualbox/hostonlynet.go b/virtualbox/hostonlynet.go new file mode 100644 index 0000000..305c245 --- /dev/null +++ b/virtualbox/hostonlynet.go @@ -0,0 +1,126 @@ +package virtualbox + +import ( + "bufio" + "errors" + "fmt" + "net" + "regexp" + "strconv" + "strings" +) + +var ( + reHostonlyInterfaceCreated = regexp.MustCompile(`Interface '(.+)' was successfully created`) +) + +var ( + ErrHostonlyInterfaceCreation = errors.New("failed to create hostonly interface") +) + +// Host-only network. +type HostonlyNet struct { + Name string + GUID string + DHCP bool + IPv4 net.IPNet + IPv6 net.IPNet + HwAddr net.HardwareAddr + Medium string + Status string + NetworkName string // referenced in DHCP.NetworkName +} + +// CreateHostonlyNet creates a new host-only network. +func CreateHostonlyNet() (*HostonlyNet, error) { + out, err := vbmOut("hostonlyif", "create") + if err != nil { + return nil, err + } + res := reHostonlyInterfaceCreated.FindStringSubmatch(string(out)) + if res == nil { + return nil, ErrHostonlyInterfaceCreation + } + return &HostonlyNet{Name: res[1]}, nil +} + +// Config changes the configuration of the host-only network. +func (n *HostonlyNet) Config() error { + if n.IPv4.IP != nil && n.IPv4.Mask != nil { + if err := vbm("hostonlyif", "ipconfig", n.Name, "--ip", n.IPv4.IP.String(), "--netmask", net.IP(n.IPv4.Mask).String()); err != nil { + return err + } + } + + if n.IPv6.IP != nil && n.IPv6.Mask != nil { + prefixLen, _ := n.IPv6.Mask.Size() + if err := vbm("hostonlyif", "ipconfig", n.Name, "--ipv6", n.IPv6.IP.String(), "--netmasklengthv6", fmt.Sprintf("%d", prefixLen)); err != nil { + return err + } + } + + if n.DHCP { + vbm("hostonlyif", "ipconfig", n.Name, "--dhcp") // not implemented as of VirtualBox 4.3 + } + + return nil +} + +// HostonlyNets gets all host-only networks in a map keyed by HostonlyNet.NetworkName. +func HostonlyNets() (map[string]*HostonlyNet, error) { + out, err := vbmOut("list", "hostonlyifs") + if err != nil { + return nil, err + } + s := bufio.NewScanner(strings.NewReader(out)) + m := map[string]*HostonlyNet{} + n := &HostonlyNet{} + for s.Scan() { + line := s.Text() + if line == "" { + m[n.NetworkName] = n + n = &HostonlyNet{} + continue + } + res := reColonLine.FindStringSubmatch(line) + if res == nil { + continue + } + switch key, val := res[1], res[2]; key { + case "Name": + n.Name = val + case "GUID": + n.GUID = val + case "DHCP": + n.DHCP = (val != "Disabled") + case "IPAddress": + n.IPv4.IP = net.ParseIP(val) + case "NetworkMask": + n.IPv4.Mask = ParseIPv4Mask(val) + case "IPV6Address": + n.IPv6.IP = net.ParseIP(val) + case "IPV6NetworkMaskPrefixLength": + l, err := strconv.ParseUint(val, 10, 7) + if err != nil { + return nil, err + } + n.IPv6.Mask = net.CIDRMask(int(l), net.IPv6len*8) + case "HardwareAddress": + mac, err := net.ParseMAC(val) + if err != nil { + return nil, err + } + n.HwAddr = mac + case "MediumType": + n.Medium = val + case "Status": + n.Status = val + case "VBoxNetworkName": + n.NetworkName = val + } + } + if err := s.Err(); err != nil { + return nil, err + } + return m, nil +} diff --git a/virtualbox/hostonlynet_test.go b/virtualbox/hostonlynet_test.go new file mode 100644 index 0000000..020124a --- /dev/null +++ b/virtualbox/hostonlynet_test.go @@ -0,0 +1,13 @@ +package virtualbox + +import "testing" + +func TestHostonlyNets(t *testing.T) { + m, err := HostonlyNets() + if err != nil { + t.Fatal(err) + } + for _, n := range m { + t.Logf("%+v", n) + } +} diff --git a/virtualbox/machine.go b/virtualbox/machine.go new file mode 100644 index 0000000..5fb08ac --- /dev/null +++ b/virtualbox/machine.go @@ -0,0 +1,401 @@ +package virtualbox + +import ( + "bufio" + "fmt" + "path/filepath" + "strconv" + "strings" + "time" +) + +type MachineState string + +const ( + Poweroff = MachineState("poweroff") + Running = MachineState("running") + Paused = MachineState("paused") + Saved = MachineState("saved") + Aborted = MachineState("aborted") +) + +type Flag int + +// Flag names in lowercases to be consistent with VBoxManage options. +const ( + F_acpi Flag = 1 << iota + F_ioapic + F_rtcuseutc + F_cpuhotplug + F_pae + F_longmode + F_synthcpu + F_hpet + F_hwvirtex + F_triplefaultreset + F_nestedpaging + F_largepages + F_vtxvpid + F_vtxux + F_accelerate3d +) + +// Convert bool to "on"/"off" +func bool2string(b bool) string { + if b { + return "on" + } + return "off" +} + +// Test if flag is set. Return "on" or "off". +func (f Flag) Get(o Flag) string { + return bool2string(f&o == o) +} + +// Machine information. +type Machine struct { + Name string + UUID string + State MachineState + CPUs uint + Memory uint // main memory (in MB) + VRAM uint // video memory (in MB) + CfgFile string + BaseFolder string + OSType string + Flag Flag + BootOrder []string // max 4 slots, each in {none|floppy|dvd|disk|net} +} + +// Refresh reloads the machine information. +func (m *Machine) Refresh() error { + id := m.Name + if id == "" { + id = m.UUID + } + mm, err := GetMachine(id) + if err != nil { + return err + } + *m = *mm + return nil +} + +// Start starts the machine. +func (m *Machine) Start() error { + switch m.State { + case Paused: + return vbm("controlvm", m.Name, "resume") + case Poweroff, Saved, Aborted: + return vbm("startvm", m.Name, "--type", "headless") + } + return nil +} + +// Suspend suspends the machine and saves its state to disk. +func (m *Machine) Save() error { + switch m.State { + case Paused: + if err := m.Start(); err != nil { + return err + } + case Poweroff, Aborted, Saved: + return nil + } + return vbm("controlvm", m.Name, "savestate") +} + +// Pause pauses the execution of the machine. +func (m *Machine) Pause() error { + switch m.State { + case Paused, Poweroff, Aborted, Saved: + return nil + } + return vbm("controlvm", m.Name, "pause") +} + +// Stop gracefully stops the machine. +func (m *Machine) Stop() error { + switch m.State { + case Poweroff, Aborted, Saved: + return nil + case Paused: + if err := m.Start(); err != nil { + return err + } + } + + for m.State != Poweroff { // busy wait until the machine is stopped + if err := vbm("controlvm", m.Name, "acpipowerbutton"); err != nil { + return err + } + time.Sleep(1 * time.Second) + if err := m.Refresh(); err != nil { + return err + } + } + return nil +} + +// Poweroff forcefully stops the machine. State is lost and might corrupt the disk image. +func (m *Machine) Poweroff() error { + switch m.State { + case Poweroff, Aborted, Saved: + return nil + } + return vbm("controlvm", m.Name, "poweroff") +} + +// Restart gracefully restarts the machine. +func (m *Machine) Restart() error { + switch m.State { + case Paused, Saved: + if err := m.Start(); err != nil { + return err + } + } + if err := m.Stop(); err != nil { + return err + } + return m.Start() +} + +// Reset forcefully restarts the machine. State is lost and might corrupt the disk image. +func (m *Machine) Reset() error { + switch m.State { + case Paused, Saved: + if err := m.Start(); err != nil { + return err + } + } + return vbm("controlvm", m.Name, "reset") +} + +// Delete deletes the machine and associated disk images. +func (m *Machine) Delete() error { + if err := m.Poweroff(); err != nil { + return err + } + return vbm("unregistervm", m.Name, "--delete") +} + +// GetMachine finds a machine by its name or UUID. +func GetMachine(id string) (*Machine, error) { + stdout, stderr, err := vbmOutErr("showvminfo", id, "--machinereadable") + if err != nil { + if reMachineNotFound.FindString(stderr) != "" { + return nil, ErrMachineNotExist + } + return nil, err + } + s := bufio.NewScanner(strings.NewReader(stdout)) + m := &Machine{} + for s.Scan() { + res := reVMInfoLine.FindStringSubmatch(s.Text()) + if res == nil { + continue + } + key := res[1] + if key == "" { + key = res[2] + } + val := res[3] + if val == "" { + val = res[4] + } + + switch key { + case "name": + m.Name = val + case "UUID": + m.UUID = val + case "VMState": + m.State = MachineState(val) + case "memory": + n, err := strconv.ParseUint(val, 10, 32) + if err != nil { + return nil, err + } + m.Memory = uint(n) + case "cpus": + n, err := strconv.ParseUint(val, 10, 32) + if err != nil { + return nil, err + } + m.CPUs = uint(n) + case "vram": + n, err := strconv.ParseUint(val, 10, 32) + if err != nil { + return nil, err + } + m.VRAM = uint(n) + case "CfgFile": + m.CfgFile = val + m.BaseFolder = filepath.Dir(val) + } + } + if err := s.Err(); err != nil { + return nil, err + } + return m, nil +} + +// ListMachines lists all registered machines. +func ListMachines() ([]*Machine, error) { + out, err := vbmOut("list", "vms") + if err != nil { + return nil, err + } + ms := []*Machine{} + s := bufio.NewScanner(strings.NewReader(out)) + for s.Scan() { + res := reVMNameUUID.FindStringSubmatch(s.Text()) + if res == nil { + continue + } + m, err := GetMachine(res[1]) + if err != nil { + return nil, err + } + ms = append(ms, m) + } + if err := s.Err(); err != nil { + return nil, err + } + return ms, nil +} + +// CreateMachine creates a new machine. If basefolder is empty, use default. +func CreateMachine(name, basefolder string) (*Machine, error) { + if name == "" { + return nil, fmt.Errorf("machine name is empty") + } + + // Check if a machine with the given name already exists. + ms, err := ListMachines() + if err != nil { + return nil, err + } + for _, m := range ms { + if m.Name == name { + return nil, ErrMachineExist + } + } + + // Create and register the machine. + args := []string{"createvm", "--name", name, "--register"} + if basefolder != "" { + args = append(args, "--basefolder", basefolder) + } + if err := vbm(args...); err != nil { + return nil, err + } + + m, err := GetMachine(name) + if err != nil { + return nil, err + } + + return m, nil +} + +// Modify changes the settings of the machine. +func (m *Machine) Modify() error { + args := []string{"modifyvm", m.Name, + "--firmware", "bios", + "--bioslogofadein", "off", + "--bioslogofadeout", "off", + "--bioslogodisplaytime", "0", + "--biosbootmenu", "disabled", + + "--ostype", m.OSType, + "--cpus", fmt.Sprintf("%d", m.CPUs), + "--memory", fmt.Sprintf("%d", m.Memory), + "--vram", fmt.Sprintf("%d", m.VRAM), + + "--acpi", m.Flag.Get(F_acpi), + "--ioapic", m.Flag.Get(F_ioapic), + "--rtcuseutc", m.Flag.Get(F_rtcuseutc), + "--cpuhotplug", m.Flag.Get(F_cpuhotplug), + "--pae", m.Flag.Get(F_pae), + "--longmode", m.Flag.Get(F_longmode), + "--synthcpu", m.Flag.Get(F_synthcpu), + "--hpet", m.Flag.Get(F_hpet), + "--hwvirtex", m.Flag.Get(F_hwvirtex), + "--triplefaultreset", m.Flag.Get(F_triplefaultreset), + "--nestedpaging", m.Flag.Get(F_nestedpaging), + "--largepages", m.Flag.Get(F_largepages), + "--vtxvpid", m.Flag.Get(F_vtxvpid), + "--vtxux", m.Flag.Get(F_vtxux), + "--accelerate3d", m.Flag.Get(F_accelerate3d), + } + + for i, dev := range m.BootOrder { + if i > 3 { + break // Only four slots `--boot{1,2,3,4}`. Ignore the rest. + } + args = append(args, fmt.Sprintf("--boot%d", i+1), dev) + } + if err := vbm(args...); err != nil { + return err + } + return m.Refresh() +} + +// AddNATPF adds a NAT port forarding rule to the n-th NIC with the given name. +func (m *Machine) AddNATPF(n int, name string, rule PFRule) error { + return vbm("controlvm", m.Name, fmt.Sprintf("natpf%d", n), + fmt.Sprintf("%s,%s", name, rule.Format())) +} + +// DelNATPF deletes the NAT port forwarding rule with the given name from the n-th NIC. +func (m *Machine) DelNATPF(n int, name string) error { + return vbm("controlvm", m.Name, fmt.Sprintf("natpf%d", n), "delete", name) +} + +// SetNIC set the n-th NIC. +func (m *Machine) SetNIC(n int, nic NIC) error { + args := []string{"modifyvm", m.Name, + fmt.Sprintf("--nic%d", n), string(nic.Network), + fmt.Sprintf("--nictype%d", n), string(nic.Hardware), + fmt.Sprintf("--cableconnected%d", n), "on", + } + + if nic.Network == "hostonly" { + args = append(args, fmt.Sprintf("--hostonlyadapter%d", n), nic.HostonlyAdapter) + } + return vbm(args...) +} + +// AddStorageCtl adds a storage controller with the given name. +func (m *Machine) AddStorageCtl(name string, ctl StorageController) error { + args := []string{"storagectl", m.Name, "--name", name} + if ctl.SysBus != "" { + args = append(args, "--add", string(ctl.SysBus)) + } + if ctl.Ports > 0 { + args = append(args, "--portcount", fmt.Sprintf("%d", ctl.Ports)) + } + if ctl.Chipset != "" { + args = append(args, "--controller", string(ctl.Chipset)) + } + args = append(args, "--hostiocache", bool2string(ctl.HostIOCache)) + args = append(args, "--bootable", bool2string(ctl.Bootable)) + return vbm(args...) +} + +// DelStorageCtl deletes the storage controller with the given name. +func (m *Machine) DelStorageCtl(name string) error { + return vbm("storagectl", m.Name, "--name", name, "--remove") +} + +// AttachStorage attaches a storage medium to the named storage controller. +func (m *Machine) AttachStorage(ctlName string, medium StorageMedium) error { + return vbm("storageattach", m.Name, "--storagectl", ctlName, + "--port", fmt.Sprintf("%d", medium.Port), + "--device", fmt.Sprintf("%d", medium.Device), + "--type", string(medium.DriveType), + "--medium", medium.Medium, + ) +} diff --git a/virtualbox/machine_test.go b/virtualbox/machine_test.go new file mode 100644 index 0000000..02d56d6 --- /dev/null +++ b/virtualbox/machine_test.go @@ -0,0 +1,13 @@ +package virtualbox + +import "testing" + +func TestMachine(t *testing.T) { + ms, err := ListMachines() + if err != nil { + t.Fatal(err) + } + for _, m := range ms { + t.Logf("%+v", m) + } +} diff --git a/virtualbox/natnet.go b/virtualbox/natnet.go new file mode 100644 index 0000000..41a8bc8 --- /dev/null +++ b/virtualbox/natnet.go @@ -0,0 +1,69 @@ +package virtualbox + +import ( + "bufio" + "net" + "strconv" + "strings" +) + +// A NATNet defines a NAT network. +type NATNet struct { + Name string + IPv4 net.IPNet + IPv6 net.IPNet + DHCP bool + Enabled bool +} + +// NATNets gets all NAT networks in a map keyed by NATNet.Name. +func NATNets() (map[string]NATNet, error) { + out, err := vbmOut("list", "natnets") + if err != nil { + return nil, err + } + s := bufio.NewScanner(strings.NewReader(out)) + m := map[string]NATNet{} + n := NATNet{} + for s.Scan() { + line := s.Text() + if line == "" { + m[n.Name] = n + n = NATNet{} + continue + } + res := reColonLine.FindStringSubmatch(line) + if res == nil { + continue + } + switch key, val := res[1], res[2]; key { + case "NetworkName": + n.Name = val + case "IP": + n.IPv4.IP = net.ParseIP(val) + case "Network": + _, ipnet, err := net.ParseCIDR(val) + if err != nil { + return nil, err + } + n.IPv4.Mask = ipnet.Mask + case "IPv6 Prefix": + if val == "" { + continue + } + l, err := strconv.ParseUint(val, 10, 7) + if err != nil { + return nil, err + } + n.IPv6.Mask = net.CIDRMask(int(l), net.IPv6len*8) + case "DHCP Enabled": + n.DHCP = (val == "Yes") + case "Enabled": + n.Enabled = (val == "Yes") + } + } + if err := s.Err(); err != nil { + return nil, err + } + return m, nil +} diff --git a/virtualbox/natnet_test.go b/virtualbox/natnet_test.go new file mode 100644 index 0000000..c12052b --- /dev/null +++ b/virtualbox/natnet_test.go @@ -0,0 +1,11 @@ +package virtualbox + +import "testing" + +func TestNATNets(t *testing.T) { + m, err := NATNets() + if err != nil { + t.Fatal(err) + } + t.Logf("%+v", m) +} diff --git a/virtualbox/nic.go b/virtualbox/nic.go new file mode 100644 index 0000000..0569be5 --- /dev/null +++ b/virtualbox/nic.go @@ -0,0 +1,33 @@ +package virtualbox + +// NIC represents a virtualized network interface card. +type NIC struct { + Network NICNetwork + Hardware NICHardware + HostonlyAdapter string +} + +// NICNetwork represents the type of NIC networks. +type NICNetwork string + +const ( + NICNetAbsent = NICNetwork("none") + NICNetDisconnected = NICNetwork("null") + NICNetNAT = NICNetwork("nat") + NICNetBridged = NICNetwork("bridged") + NICNetInternal = NICNetwork("intnet") + NICNetHostonly = NICNetwork("hostonly") + NICNetGeneric = NICNetwork("generic") +) + +// NICHardware represents the type of NIC hardware. +type NICHardware string + +const ( + AMDPCNetPCIII = NICHardware("Am79C970A") + AMDPCNetFASTIII = NICHardware("Am79C973") + IntelPro1000MTDesktop = NICHardware("82540EM") + IntelPro1000TServer = NICHardware("82543GC") + IntelPro1000MTServer = NICHardware("82545EM") + VirtIO = NICHardware("virtio") +) diff --git a/virtualbox/pfrule.go b/virtualbox/pfrule.go new file mode 100644 index 0000000..89b9de7 --- /dev/null +++ b/virtualbox/pfrule.go @@ -0,0 +1,51 @@ +package virtualbox + +import ( + "fmt" + "net" +) + +// PFRule represents a port forwarding rule. +type PFRule struct { + Proto PFProto + HostIP net.IP // can be nil to match any host interface + HostPort uint16 + GuestIP net.IP // can be nil if guest IP is leased from built-in DHCP + GuestPort uint16 +} + +// PFProto represents the protocol of a port forwarding rule. +type PFProto string + +const ( + PFTCP = PFProto("tcp") + PFUDP = PFProto("udp") +) + +// String returns a human-friendly representation of the port forwarding rule. +func (r PFRule) String() string { + hostip := "" + if r.HostIP != nil { + hostip = r.HostIP.String() + } + guestip := "" + if r.GuestIP != nil { + guestip = r.GuestIP.String() + } + return fmt.Sprintf("%s://%s:%d --> %s:%d", + r.Proto, hostip, r.HostPort, + guestip, r.GuestPort) +} + +// Format returns the string needed as a command-line argument to VBoxManage. +func (r PFRule) Format() string { + hostip := "" + if r.HostIP != nil { + hostip = r.HostIP.String() + } + guestip := "" + if r.GuestIP != nil { + guestip = r.GuestIP.String() + } + return fmt.Sprintf("%s,%s,%d,%s,%d", r.Proto, hostip, r.HostPort, guestip, r.GuestPort) +} diff --git a/virtualbox/storage.go b/virtualbox/storage.go new file mode 100644 index 0000000..ae129f4 --- /dev/null +++ b/virtualbox/storage.go @@ -0,0 +1,51 @@ +package virtualbox + +// StorageController represents a virtualized storage controller. +type StorageController struct { + SysBus SystemBus + Ports uint // SATA port count 1--30 + Chipset StorageControllerChipset + HostIOCache bool + Bootable bool +} + +// SystemBus represents the system bus of a storage controller. +type SystemBus string + +const ( + SysBusIDE = SystemBus("ide") + SysBusSATA = SystemBus("sata") + SysBusSCSI = SystemBus("scsi") + SysBusFloppy = SystemBus("floppy") +) + +// StorageControllerChipset represents the hardware of a storage controller. +type StorageControllerChipset string + +const ( + CtrlLSILogic = StorageControllerChipset("LSILogic") + CtrlLSILogicSAS = StorageControllerChipset("LSILogicSAS") + CtrlBusLogic = StorageControllerChipset("BusLogic") + CtrlIntelAHCI = StorageControllerChipset("IntelAHCI") + CtrlPIIX3 = StorageControllerChipset("PIIX3") + CtrlPIIX4 = StorageControllerChipset("PIIX4") + CtrlICH6 = StorageControllerChipset("ICH6") + CtrlI82078 = StorageControllerChipset("I82078") +) + +// StorageMedium represents the storage medium attached to a storage controller. +type StorageMedium struct { + Port uint + Device uint + DriveType DriveType + Medium string // none|emptydrive|||iscsi +} + +// DriveType represents the hardware type of a drive. +type DriveType string + +const ( + DriveDVD = DriveType("dvddrive") + DriveHDD = DriveType("hdd") + DriveFDD = DriveType("fdd") +) diff --git a/virtualbox/util.go b/virtualbox/util.go new file mode 100644 index 0000000..0df71e5 --- /dev/null +++ b/virtualbox/util.go @@ -0,0 +1,13 @@ +package virtualbox + +import "net" + +// ParseIPv4Mask parses IPv4 netmask written in IP form (e.g. 255.255.255.0). +// This function should really belong to the net package. +func ParseIPv4Mask(s string) net.IPMask { + mask := net.ParseIP(s) + if mask == nil { + return nil + } + return net.IPv4Mask(mask[12], mask[13], mask[14], mask[15]) +} diff --git a/virtualbox/vbm.go b/virtualbox/vbm.go new file mode 100644 index 0000000..dd0df6f --- /dev/null +++ b/virtualbox/vbm.go @@ -0,0 +1,88 @@ +package virtualbox + +import ( + "bytes" + "errors" + "log" + "os" + "os/exec" + "path/filepath" + "regexp" + "runtime" + "strings" +) + +var ( + VBM string // Path to VBoxManage utility. + Verbose bool // Verbose mode. +) + +func init() { + VBM = "VBoxManage" + if p := os.Getenv("VBOX_INSTALL_PATH"); p != "" && runtime.GOOS == "windows" { + VBM = filepath.Join(p, "VBoxManage.exe") + } +} + +var ( + reVMNameUUID = regexp.MustCompile(`"(.+)" {([0-9a-f-]+)}`) + reVMInfoLine = regexp.MustCompile(`(?:"(.+)"|(.+))=(?:"(.*)"|(.*))`) + reColonLine = regexp.MustCompile(`(.+):\s+(.*)`) + reMachineNotFound = regexp.MustCompile(`Could not find a registered machine named '(.+)'`) +) + +var ( + ErrMachineExist = errors.New("machine already exists") + ErrMachineNotExist = errors.New("machine does not exist") + ErrVBMNotFound = errors.New("VBoxManage not found") +) + +func vbm(args ...string) error { + cmd := exec.Command(VBM, args...) + if Verbose { + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + log.Printf("executing: %v %v", VBM, strings.Join(args, " ")) + } + if err := cmd.Run(); err != nil { + if ee, ok := err.(*exec.Error); ok && ee == exec.ErrNotFound { + return ErrVBMNotFound + } + return err + } + return nil +} + +func vbmOut(args ...string) (string, error) { + cmd := exec.Command(VBM, args...) + if Verbose { + cmd.Stderr = os.Stderr + log.Printf("executing: %v %v", VBM, strings.Join(args, " ")) + } + + b, err := cmd.Output() + if err != nil { + if ee, ok := err.(*exec.Error); ok && ee == exec.ErrNotFound { + err = ErrVBMNotFound + } + } + return string(b), err +} + +func vbmOutErr(args ...string) (string, string, error) { + cmd := exec.Command(VBM, args...) + if Verbose { + log.Printf("executing: %v %v", VBM, strings.Join(args, " ")) + } + var stdout bytes.Buffer + var stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + err := cmd.Run() + if err != nil { + if ee, ok := err.(*exec.Error); ok && ee == exec.ErrNotFound { + err = ErrVBMNotFound + } + } + return stdout.String(), stderr.String(), err +} diff --git a/virtualbox/vbm_test.go b/virtualbox/vbm_test.go new file mode 100644 index 0000000..0355a11 --- /dev/null +++ b/virtualbox/vbm_test.go @@ -0,0 +1,17 @@ +package virtualbox + +import ( + "testing" +) + +func init() { + Verbose = true +} + +func TestVBMOut(t *testing.T) { + b, err := vbmOut("list", "vms") + if err != nil { + t.Fatal(err) + } + t.Logf("%s", b) +}