diff --git a/cmds.go b/cmds.go index c13312b..510b9d5 100644 --- a/cmds.go +++ b/cmds.go @@ -11,6 +11,7 @@ import ( "time" _ "github.com/boot2docker/boot2docker-cli/dummy" + _ "github.com/boot2docker/boot2docker-cli/fusion" _ "github.com/boot2docker/boot2docker-cli/virtualbox" "github.com/boot2docker/boot2docker-cli/driver" diff --git a/driver/driver.go b/driver/driver.go index 9b5524a..b27d140 100644 --- a/driver/driver.go +++ b/driver/driver.go @@ -42,6 +42,7 @@ type Machine interface { AttachStorage(ctlName string, medium StorageMedium) error GetState() MachineState GetName() string + GetHostname() string GetSerialFile() string GetDockerPort() uint GetSSHPort() uint diff --git a/dummy/machine.go b/dummy/machine.go index cfc3f1d..ce34b35 100644 --- a/dummy/machine.go +++ b/dummy/machine.go @@ -47,8 +47,9 @@ func ConfigFlags(B2D *driver.MachineConfig, flags *flag.FlagSet) error { // Machine information. type Machine struct { - Name string UUID string + Name string + Hostname string State driver.MachineState CPUs uint Memory uint // main memory (in MB) @@ -122,6 +123,11 @@ func (m *Machine) GetName() string { return m.Name } +// Get machine hostname +func (m *Machine) GetHostname() string { + return m.Hostname +} + // Get current state func (m *Machine) GetState() driver.MachineState { return m.State diff --git a/fusion/machine.go b/fusion/machine.go new file mode 100644 index 0000000..69e3257 --- /dev/null +++ b/fusion/machine.go @@ -0,0 +1,278 @@ +package fusion + +import ( + "bufio" + "fmt" + "os" + "path/filepath" + "strconv" + "strings" + "text/template" + + "github.com/boot2docker/boot2docker-cli/driver" + "github.com/ogier/pflag" +) + +var ( + verbose bool // Verbose mode. + cfg DriverCfg +) + +type DriverCfg struct { + VMRUN string // Path to vmrun utility. + VDISKMAN string // Path to vdiskmanager utility. +} + +func init() { + if err := driver.Register("fusion", InitFunc); err != nil { + fmt.Fprintf(os.Stderr, "Failed to initialize driver. Error : %s", err.Error()) + os.Exit(1) + } + if err := driver.RegisterConfig("fusion", ConfigFlags); err != nil { + fmt.Fprintf(os.Stderr, "Failed to initialize driver config. Error : %s", err.Error()) + os.Exit(1) + } +} + +// Initialize the Machine. +func InitFunc(mc *driver.MachineConfig) (driver.Machine, error) { + verbose = mc.Verbose + + m, err := GetMachine(getVMX(mc)) + if err != nil && mc.Init == true { + return CreateMachine(mc) + } + return m, err +} + +// Add cmdline params for this driver +func ConfigFlags(mc *driver.MachineConfig, flags *pflag.FlagSet) error { + cfg.VMRUN = "/Applications/VMware Fusion.app/Contents/Library/vmrun" + flags.StringVar(&cfg.VMRUN, "vmrun", cfg.VMRUN, "path to vmrun utility.") + + cfg.VDISKMAN = "/Applications/VMware Fusion.app/Contents/Library/vmware-vdiskmanager" + flags.StringVar(&cfg.VDISKMAN, "vmdiskman", cfg.VDISKMAN, "path to vdiskmanager utility.") + + return nil +} + +// Machine information. +type Machine struct { + Name string + State driver.MachineState + CPUs uint64 + Memory uint64 // main memory (in MB) + VMX string + OSType string +} + +// Refresh reloads the machine information. +func (m *Machine) Refresh() error { + mm, err := GetMachine(m.VMX) + if err != nil { + return err + } + *m = *mm + return nil +} + +// Start starts the machine. +func (m *Machine) Start() error { + vmrun("start", m.VMX, "nogui") + return nil +} + +// Suspend suspends the machine and saves its state to disk. +func (m *Machine) Save() error { + vmrun("suspend", m.VMX, "nogui") + return nil +} + +// Pause pauses the execution of the machine. +func (m *Machine) Pause() error { + vmrun("pause", m.VMX, "nogui") + return nil +} + +// Stop gracefully stops the machine. +func (m *Machine) Stop() error { + vmrun("stop", m.VMX, "nogui") + return nil +} + +// Poweroff forcefully stops the machine. State is lost and might corrupt the disk image. +func (m *Machine) Poweroff() error { + vmrun("stop", m.VMX, "nogui") + return nil +} + +// Restart gracefully restarts the machine. +func (m *Machine) Restart() error { + vmrun("reset", m.VMX, "nogui") + return nil +} + +// Reset forcefully restarts the machine. State is lost and might corrupt the disk image. +func (m *Machine) Reset() error { + vmrun("reset", m.VMX, "nogui") + return nil +} + +// Get vm name +func (m *Machine) GetName() string { + return m.Name +} + +// Get vm hostname +func (m *Machine) GetHostname() string { + stdout, _, _ := vmrun("getGuestIPAddress", m.VMX) + return strings.TrimSpace(stdout) +} + +// Get current state +func (m *Machine) GetState() driver.MachineState { + return m.State +} + +// Get serial file +func (m *Machine) GetSerialFile() string { + return "" +} + +// Get Docker port +func (m *Machine) GetDockerPort() uint { + return 2375 +} + +// Get SSH port +func (m *Machine) GetSSHPort() uint { + return 22 +} + +// Delete deletes the machine and associated disk images. +func (m *Machine) Delete() error { + vmrun("deleteVM", m.VMX, "nogui") + return nil +} + +// Modify changes the settings of the machine. +func (m *Machine) Modify() error { + fmt.Printf("Hot modify not supported") + 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 driver.PFRule) error { + fmt.Println("Add NAT PF") + return nil +} + +// 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 { + fmt.Println("Del NAT PF") + return nil +} + +// SetNIC set the n-th NIC. +func (m *Machine) SetNIC(n int, nic driver.NIC) error { + fmt.Println("Set NIC") + return nil +} + +// AddStorageCtl adds a storage controller with the given name. +func (m *Machine) AddStorageCtl(name string, ctl driver.StorageController) error { + fmt.Println("Add storage ctl") + return nil +} + +// DelStorageCtl deletes the storage controller with the given name. +func (m *Machine) DelStorageCtl(name string) error { + fmt.Println("Del storage ctl") + return nil +} + +// AttachStorage attaches a storage medium to the named storage controller. +func (m *Machine) AttachStorage(ctlName string, medium driver.StorageMedium) error { + fmt.Println("Attach storage") + return nil +} + +// GetMachine finds a machine. +func GetMachine(vmx string) (*Machine, error) { + if _, err := os.Stat(vmx); os.IsNotExist(err) { + return nil, ErrMachineNotExist + } + + m := &Machine{VMX: vmx, State: driver.Poweroff} + + // VMRUN only tells use if the vm is running or not + if stdout, _, _ := vmrun("list"); strings.Contains(stdout, m.VMX) { + m.State = driver.Running + } + + // Parse the vmx file + vmxfile, err := os.Open(vmx) + if err != nil { + return m, err + } + defer vmxfile.Close() + + vmxscan := bufio.NewScanner(vmxfile) + for vmxscan.Scan() { + if vmxtokens := strings.Split(vmxscan.Text(), " = "); len(vmxtokens) > 1 { + vmxkey := strings.TrimSpace(vmxtokens[0]) + vmxvalue, _ := strconv.Unquote(vmxtokens[1]) + switch vmxkey { + case "displayName": + m.Name = vmxvalue + case "guestOS": + m.OSType = vmxvalue + case "memsize": + m.Memory, _ = strconv.ParseUint(vmxvalue, 10, 0) + case "numvcpus": + m.CPUs, _ = strconv.ParseUint(vmxvalue, 10, 0) + } + } + } + return m, nil +} + +// CreateMachine creates a new virtual machine. +func CreateMachine(mc *driver.MachineConfig) (*Machine, error) { + if err := os.MkdirAll(getBaseFolder(mc), 0755); err != nil { + return nil, err + } + + if _, err := os.Stat(getVMX(mc)); err == nil { + return nil, ErrMachineExist + } + + // Generate vmx config file from template + vmxt := template.Must(template.New("vmx").Parse(vmx)) + vmxfile, err := os.Create(getVMX(mc)) + if err != nil { + return nil, err + } + vmxt.Execute(vmxfile, mc) + + // Generate vmdk file + diskImg := filepath.Join(getBaseFolder(mc), fmt.Sprintf("%s.vmdk", mc.VM)) + if _, err := os.Stat(diskImg); err != nil { + if !os.IsNotExist(err) { + return nil, err + } + + if err := vdiskmanager(diskImg, mc.DiskSize); err != nil { + return nil, err + } + } + + return nil, nil +} + +func getBaseFolder(mc *driver.MachineConfig) string { + return filepath.Join(mc.Dir, mc.VM) +} +func getVMX(mc *driver.MachineConfig) string { + return filepath.Join(getBaseFolder(mc), fmt.Sprintf("%s.vmx", mc.VM)) +} diff --git a/fusion/vmrun.go b/fusion/vmrun.go new file mode 100644 index 0000000..ba36419 --- /dev/null +++ b/fusion/vmrun.go @@ -0,0 +1,48 @@ +package fusion + +import ( + "bytes" + "errors" + "fmt" + "log" + "os/exec" + "strings" +) + +var ( + ErrMachineExist = errors.New("machine already exists") + ErrMachineNotExist = errors.New("machine does not exist") + ErrVMRUNNotFound = errors.New("VMRUN not found") +) + +func vmrun(args ...string) (string, string, error) { + cmd := exec.Command(cfg.VMRUN, args...) + if verbose { + log.Printf("executing: %v %v", cfg.VMRUN, strings.Join(args, " ")) + } + + var stdout bytes.Buffer + var stderr bytes.Buffer + cmd.Stdout, cmd.Stderr = &stdout, &stderr + + err := cmd.Run() + if err != nil { + if ee, ok := err.(*exec.Error); ok && ee == exec.ErrNotFound { + err = ErrVMRUNNotFound + } + } + + return stdout.String(), stderr.String(), err +} + +// Make a vmdk disk image with the given size (in MB). +func vdiskmanager(dest string, size uint) error { + cmd := exec.Command(cfg.VDISKMAN, "-c", "-t", "0", "-s", fmt.Sprintf("%dMB", size), "-a", "lsilogic", dest) + + if stdout := cmd.Run(); stdout != nil { + if ee, ok := stdout.(*exec.Error); ok && ee == exec.ErrNotFound { + return ErrVMRUNNotFound + } + } + return nil +} diff --git a/fusion/vmx.go b/fusion/vmx.go new file mode 100644 index 0000000..2b467ba --- /dev/null +++ b/fusion/vmx.go @@ -0,0 +1,31 @@ +package fusion + +const vmx = ` +.encoding = "UTF-8" +config.version = "8" +displayName = "{{.VM}}" +ethernet0.addressType = "generated" +ethernet0.connectionType = "nat" +ethernet0.linkStatePropagation.enable = "TRUE" +ethernet0.present = "TRUE" +ethernet0.virtualDev = "e1000" +ethernet0.wakeOnPcktRcv = "FALSE" +floppy0.present = "FALSE" +guestOS = "other26xlinux-64" +hpet0.present = "TRUE" +ide1:0.deviceType = "cdrom-image" +ide1:0.fileName = "{{.ISO}}" +ide1:0.present = "TRUE" +mem.hotadd = "TRUE" +memsize = "{{.Memory}}" +powerType.powerOff = "soft" +powerType.powerOn = "soft" +powerType.reset = "soft" +powerType.suspend = "soft" +scsi0.present = "TRUE" +scsi0.virtualDev = "lsilogic" +scsi0:0.fileName = "{{.VM}}.vmdk" +scsi0:0.present = "TRUE" +virtualHW.productCompatibility = "hosted" +virtualHW.version = "10" +` diff --git a/util.go b/util.go index fa072aa..7809835 100644 --- a/util.go +++ b/util.go @@ -168,7 +168,6 @@ func reader(r io.Reader) { } func getSSHCommand(m driver.Machine, args ...string) *exec.Cmd { - DefaultSSHArgs := []string{ "-o", "IdentitiesOnly=yes", "-o", "StrictHostKeyChecking=no", @@ -176,7 +175,7 @@ func getSSHCommand(m driver.Machine, args ...string) *exec.Cmd { "-o", "LogLevel=quiet", // suppress "Warning: Permanently added '[localhost]:2022' (ECDSA) to the list of known hosts." "-p", fmt.Sprintf("%d", m.GetSSHPort()), "-i", B2D.SSHKey, - "docker@localhost", + fmt.Sprintf("docker@%s", m.GetHostname()), } sshArgs := append(DefaultSSHArgs, args...) diff --git a/virtualbox/machine.go b/virtualbox/machine.go index 2009d01..6523073 100644 --- a/virtualbox/machine.go +++ b/virtualbox/machine.go @@ -305,6 +305,11 @@ func (m *Machine) GetName() string { return m.Name } +// Get machine hostname +func (m *Machine) GetHostname() string { + return "localhost" +} + // Get current state func (m *Machine) GetState() driver.MachineState { return m.State