diff --git a/VERSION b/VERSION index 18fa8e7..968e750 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v1.3.0 +v1.3.2 diff --git a/cmds.go b/cmds.go index c13312b..d41e263 100644 --- a/cmds.go +++ b/cmds.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "os/exec" + "path/filepath" "regexp" "runtime" "strings" @@ -12,6 +13,7 @@ import ( _ "github.com/boot2docker/boot2docker-cli/dummy" _ "github.com/boot2docker/boot2docker-cli/virtualbox" + _ "github.com/boot2docker/boot2docker-cli/vsphere" "github.com/boot2docker/boot2docker-cli/driver" ) @@ -83,7 +85,7 @@ func cmdUp() error { fmt.Println("Waiting for VM and Docker daemon to start...") //give the VM a little time to start, so we don't kill the Serial Pipe/Socket time.Sleep(time.Duration(B2D.Waittime) * time.Millisecond) - natSSH := fmt.Sprintf("localhost:%d", m.GetSSHPort()) + natSSH := fmt.Sprintf("%s:%d", m.GetAddr(), m.GetSSHPort()) IP := "" for i := 1; i < B2D.Retries; i++ { print(".") @@ -193,7 +195,14 @@ func checkEnvironment(socket, certPath string) bool { func printExport(socket, certPath string) { for name, value := range exports(socket, certPath) { - if os.Getenv(name) != value { + switch filepath.Base(os.Getenv("SHELL")) { + case "fish": + if value == "" { + fmt.Printf(" set -e %s\n", name) + } else { + fmt.Printf(" set -x %s %s\n", name, value) + } + default: // default command to export variables POSIX shells, like bash, zsh, etc. if value == "" { fmt.Printf(" unset %s\n", name) } else { @@ -329,9 +338,13 @@ func cmdInfo() error { if err != nil { return fmt.Errorf("Failed to get machine %q: %s", B2D.VM, err) } - if err := json.NewEncoder(os.Stdout).Encode(m); err != nil { + b, err := json.MarshalIndent(m, "", "\t") + if err != nil { return fmt.Errorf("Failed to encode machine %q info: %s", B2D.VM, err) } + + os.Stdout.Write(b) + return nil } @@ -345,29 +358,6 @@ func cmdStatus() error { return nil } -// tell the User the Docker socket to connect to -func cmdSocket() error { - m, err := driver.GetMachine(&B2D) - if err != nil { - return fmt.Errorf("Failed to get machine %q: %s", B2D.VM, err) - } - - if m.GetState() != driver.Running { - return fmt.Errorf("VM %q is not running.", B2D.VM) - } - - socket, err := RequestSocketFromSSH(m) - if err != nil { - return fmt.Errorf("Error requesting socket: %s\n", err) - } - - fmt.Fprintf(os.Stderr, "\n\t export DOCKER_HOST=") - fmt.Printf("%s", socket) - fmt.Fprintf(os.Stderr, "\n\n") - - return nil -} - // Call the external SSH command to login into boot2docker VM. func cmdSSH() error { m, err := driver.GetMachine(&B2D) diff --git a/config.go b/config.go index 48436bb..b9b96a8 100644 --- a/config.go +++ b/config.go @@ -171,7 +171,7 @@ func config() (*flag.FlagSet, error) { } func usageShort() { - fmt.Fprintf(os.Stderr, "Usage: %s [] {help|init|up|ssh|save|down|poweroff|reset|restart|config|status|info|ip|socket|shellinit|delete|download|upgrade|version} []\n", os.Args[0]) + fmt.Fprintf(os.Stderr, "Usage: %s [] {help|init|up|ssh|save|down|poweroff|reset|restart|config|status|info|ip|shellinit|delete|download|upgrade|version} []\n", os.Args[0]) } func usageLong(flags *flag.FlagSet) { @@ -193,8 +193,7 @@ Commands: config|cfg Show selected profile file settings. info Display detailed information of VM. ip Display the IP address of the VM's Host-only network. - socket Display the DOCKER_HOST socket to connect to. - shellinit Display the shell command to set up the Docker client. + shellinit Display the shell commands to set up the Docker client. status Display current state of VM. download Download Boot2Docker ISO image. upgrade Upgrade the Boot2Docker ISO image (restart if running). diff --git a/driver/driver.go b/driver/driver.go index 9b5524a..56b38ce 100644 --- a/driver/driver.go +++ b/driver/driver.go @@ -45,14 +45,16 @@ type Machine interface { GetSerialFile() string GetDockerPort() uint GetSSHPort() uint + GetAddr() string } var ( // All registred machines machines map[string]InitFunc - ErrNotSupported = errors.New("machine not supported") + ErrNotSupported = errors.New("driver not supported") ErrMachineNotExist = errors.New("machine not exist") + ErrMachineExist = errors.New("machine already exists") ErrPrerequisites = errors.New("prerequisites for machine not satisfied (hypervisor installed?)") ) diff --git a/dummy/machine.go b/dummy/machine.go index cfc3f1d..c591e31 100644 --- a/dummy/machine.go +++ b/dummy/machine.go @@ -122,6 +122,11 @@ func (m *Machine) GetName() string { return m.Name } +// Get machine address +func (m *Machine) GetAddr() string { + return "localhost" +} + // Get current state func (m *Machine) GetState() driver.MachineState { return m.State diff --git a/main.go b/main.go index d062da2..a4f90dd 100644 --- a/main.go +++ b/main.go @@ -65,9 +65,7 @@ func run() error { return cmdDelete() case "info": return cmdInfo() - case "socket": - return cmdSocket() - case "shellinit": + case "shellinit", "socket": return cmdShellInit() case "status": return cmdStatus() diff --git a/util.go b/util.go index fa072aa..aa5348c 100644 --- a/util.go +++ b/util.go @@ -176,7 +176,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.GetAddr()), } sshArgs := append(DefaultSSHArgs, args...) diff --git a/virtualbox/machine.go b/virtualbox/machine.go index 2009d01..ee0614b 100644 --- a/virtualbox/machine.go +++ b/virtualbox/machine.go @@ -161,6 +161,7 @@ func (f Flag) Get(o Flag) string { type Machine struct { Name string UUID string + Iso string State driver.MachineState CPUs uint Memory uint // main memory (in MB) @@ -194,10 +195,12 @@ func (m *Machine) Start() error { switch m.State { case driver.Paused: return vbm("controlvm", m.Name, "resume") - case driver.Poweroff, driver.Saved, driver.Aborted: + case driver.Poweroff, driver.Aborted: if err := m.setUpShares(); err != nil { return err } + fallthrough + case driver.Saved: return vbm("startvm", m.Name, "--type", "headless") } if err := m.Refresh(); err == nil { @@ -305,6 +308,11 @@ func (m *Machine) GetName() string { return m.Name } +// Get machine address +func (m *Machine) GetAddr() string { + return "localhost" +} + // Get current state func (m *Machine) GetState() driver.MachineState { return m.State @@ -330,7 +338,7 @@ 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, driver.ErrMachineNotExist } return nil, err } @@ -355,6 +363,8 @@ func GetMachine(id string) (*Machine, error) { m.Name = val case "UUID": m.UUID = val + case "SATA-0-0": + m.Iso = val case "VMState": m.State = driver.MachineState(val) case "memory": @@ -443,7 +453,7 @@ func CreateMachine(mc *driver.MachineConfig) (*Machine, error) { } for _, m := range machineNames { if m == mc.VM { - return nil, ErrMachineExist + return nil, driver.ErrMachineExist } } diff --git a/virtualbox/vbm.go b/virtualbox/vbm.go index 641c071..bbb5630 100644 --- a/virtualbox/vbm.go +++ b/virtualbox/vbm.go @@ -9,12 +9,18 @@ import ( "os/exec" "path/filepath" "regexp" + "runtime" "strings" "github.com/boot2docker/boot2docker-cli/driver" ) func init() { + if runtime.GOOS == "darwin" { + // remove DYLD_LIBRARY_PATH and LD_LIBRARY_PATH as they break VBoxManage on OSX + os.Setenv("DYLD_LIBRARY_PATH", "") + os.Setenv("LD_LIBRARY_PATH", "") + } } var ( @@ -25,9 +31,7 @@ var ( ) var ( - ErrMachineExist = errors.New("machine already exists") - ErrMachineNotExist = errors.New("machine does not exist") - ErrVBMNotFound = errors.New("VBoxManage not found") + ErrVBMNotFound = errors.New("VBoxManage not found") ) func vbm(args ...string) error { diff --git a/vsphere/README.md b/vsphere/README.md new file mode 100644 index 0000000..0886c1a --- /dev/null +++ b/vsphere/README.md @@ -0,0 +1,51 @@ +Boot2docker vSphere Driver +========================== + +The vSphere driver is to support running vSphere environment. + +vSphere Environment Requirement +--------------- + +The vSphere environment requires DHCP on the VM network the boot2docker VM is running on. + + +Configuration +--------------- + +The boot2docker reads the driver information from its profile, and a sample snippet configuration is provided below: + +```ini +# boot2docker profile filename: /Users/my/.boot2docker/profile +...... +Driver = "vsphere" +...... + +[DriverCfg.vsphere] +# path to the govc binary +Govc = "govc" + +# vCenter IP address +VcenterIp = "10.150.100.200" + +# vCenter Username (console should prompt for password) +VcenterUser = "root" + +# target datacenter to deploy the boot2docker virtual machine +VcenterDatacenter = "Datacenter" + +# target datastore to upload the boot2docker ISO and store the boot2docker virtual machine +VcenterDatastore = "datastore1" + +# target network to add the boot2docker virtual machine (requires DHCP) +VcenterNetwork = "VM Network" + +# (optional) required when user want to deploy to a specified host or multiple clusters/hosts exist in the environment +VcenterHostIp = "10.120.180.160" + +# (optional) required when user wants to deploy to a specified cluster or multiple clusters/hosts exist in this environment +VcenterPool = "cluster" + +# (optional) the default vm CPU number is 2 +VmCPU = 4 +``` + diff --git a/vsphere/errors/config_error.go b/vsphere/errors/config_error.go new file mode 100644 index 0000000..593fe1a --- /dev/null +++ b/vsphere/errors/config_error.go @@ -0,0 +1,18 @@ +package errors + +import "fmt" + +type IncompleteVcConfigError struct { + component string +} + +func NewIncompleteVcConfigError(component string) error { + err := IncompleteVcConfigError{ + component: component, + } + return &err +} + +func (err *IncompleteVcConfigError) Error() string { + return fmt.Sprintf("Incomplete vCenter information: missing %s", err.component) +} diff --git a/vsphere/errors/datastore_error.go b/vsphere/errors/datastore_error.go new file mode 100644 index 0000000..fcf9cc4 --- /dev/null +++ b/vsphere/errors/datastore_error.go @@ -0,0 +1,22 @@ +package errors + +import "fmt" + +type DatastoreError struct { + datastore string + operation string + reason string +} + +func NewDatastoreError(datastore, operation, reason string) error { + err := DatastoreError{ + datastore: datastore, + operation: operation, + reason: reason, + } + return &err +} + +func (err *DatastoreError) Error() string { + return fmt.Sprintf("Unable to %s on datastore %s due to %s", err.operation, err.datastore, err.reason) +} diff --git a/vsphere/errors/errors.go b/vsphere/errors/errors.go new file mode 100644 index 0000000..bb7843a --- /dev/null +++ b/vsphere/errors/errors.go @@ -0,0 +1,18 @@ +package errors + +import ( + original "errors" + "fmt" +) + +func New(message string) error { + return original.New(message) +} + +func NewWithFmt(message string, args ...interface{}) error { + return original.New(fmt.Sprintf(message, args...)) +} + +func NewWithError(message string, err error) error { + return NewWithFmt("%s: %s", message, err.Error()) +} diff --git a/vsphere/errors/govc_error.go b/vsphere/errors/govc_error.go new file mode 100644 index 0000000..b036c13 --- /dev/null +++ b/vsphere/errors/govc_error.go @@ -0,0 +1,18 @@ +package errors + +import "fmt" + +type GovcNotFoundError struct { + path string +} + +func NewGovcNotFoundError(path string) error { + err := GovcNotFoundError{ + path: path, + } + return &err +} + +func (err *GovcNotFoundError) Error() string { + return fmt.Sprintf("govc not found: %s", err.path) +} diff --git a/vsphere/errors/guest_error.go b/vsphere/errors/guest_error.go new file mode 100644 index 0000000..9c5df4a --- /dev/null +++ b/vsphere/errors/guest_error.go @@ -0,0 +1,22 @@ +package errors + +import "fmt" + +type GuestError struct { + vm string + operation string + reason string +} + +func NewGuestError(vm, operation, reason string) error { + err := GuestError{ + vm: vm, + operation: operation, + reason: reason, + } + return &err +} + +func (err *GuestError) Error() string { + return fmt.Sprintf("Unable to %s on vm %s due to %s", err.operation, err.vm, err.reason) +} diff --git a/vsphere/errors/login_error.go b/vsphere/errors/login_error.go new file mode 100644 index 0000000..23abc26 --- /dev/null +++ b/vsphere/errors/login_error.go @@ -0,0 +1,13 @@ +package errors + +type InvalidLoginError struct { +} + +func NewInvalidLoginError() error { + err := InvalidLoginError{} + return &err +} + +func (err *InvalidLoginError) Error() string { + return "cannot complete operation due to incorrect vSphere username or password" +} diff --git a/vsphere/errors/state_error.go b/vsphere/errors/state_error.go new file mode 100644 index 0000000..964b9bf --- /dev/null +++ b/vsphere/errors/state_error.go @@ -0,0 +1,18 @@ +package errors + +import "fmt" + +type InvalidStateError struct { + vm string +} + +func NewInvalidStateError(vm string) error { + err := InvalidStateError{ + vm: vm, + } + return &err +} + +func (err *InvalidStateError) Error() string { + return fmt.Sprintf("Machine %s state invalid", err.vm) +} diff --git a/vsphere/errors/vm_error.go b/vsphere/errors/vm_error.go new file mode 100644 index 0000000..7d12d16 --- /dev/null +++ b/vsphere/errors/vm_error.go @@ -0,0 +1,22 @@ +package errors + +import "fmt" + +type VmError struct { + operation string + vm string + reason string +} + +func NewVmError(operation, vm, reason string) error { + err := VmError{ + vm: vm, + operation: operation, + reason: reason, + } + return &err +} + +func (err *VmError) Error() string { + return fmt.Sprintf("Unable to %s docker host %s: %s", err.operation, err.vm, err.reason) +} diff --git a/vsphere/govc.go b/vsphere/govc.go new file mode 100644 index 0000000..c6e24ec --- /dev/null +++ b/vsphere/govc.go @@ -0,0 +1,55 @@ +package vsphere + +import ( + "bytes" + "log" + "os" + "os/exec" + "strings" + + "github.com/boot2docker/boot2docker-cli/vsphere/errors" +) + +func init() { +} + +func govc(args ...string) error { + err := lookPath(cfg.Govc) + if err != nil { + return errors.NewGovcNotFoundError(cfg.Govc) + } + + cmd := exec.Command(cfg.Govc, args...) + if verbose { + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + log.Printf("executing: %v %v", cfg.Govc, strings.Join(args, " ")) + } + if err := cmd.Run(); err != nil { + return err + } + return nil +} + +func govcOutErr(args ...string) (string, string, error) { + err := lookPath(cfg.Govc) + if err != nil { + return "", "", errors.NewGovcNotFoundError(cfg.Govc) + } + + cmd := exec.Command(cfg.Govc, args...) + if verbose { + log.Printf("executing: %v %v", cfg.Govc, strings.Join(args, " ")) + } + var stdout bytes.Buffer + var stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + err = cmd.Run() + return stdout.String(), stderr.String(), err +} + +func lookPath(file string) error { + _, err := exec.LookPath(file) + return err +} diff --git a/vsphere/machine.go b/vsphere/machine.go new file mode 100644 index 0000000..13aec57 --- /dev/null +++ b/vsphere/machine.go @@ -0,0 +1,462 @@ +package vsphere + +import ( + "fmt" + "os" + "strconv" + "strings" + + "github.com/boot2docker/boot2docker-cli/driver" + "github.com/boot2docker/boot2docker-cli/vsphere/errors" + flag "github.com/ogier/pflag" +) + +type DriverCfg struct { + Govc string // Path to govc binary + VcenterIp string // vCenter URL + VcenterUser string // vCenter User + VcenterDC string // target vCenter Datacenter + VcenterDS string // target vCenter Datastore + VcenterNet string // vCenter VM Network + VcenterPool string // target vCenter Resource Pool + VcenterHostIp string // target vCenter Host Ip + Cpu string // CPU number of the virtual machine +} + +var ( + verbose bool // Verbose mode (Local copy of B2D.Verbose). + cfg DriverCfg +) + +const ( + DATASTORE_DIR = "boot2docker-iso" + DATASTORE_ISO_NAME = "boot2docker.iso" + DEFAULT_CPU_NUMBER = 2 +) + +func init() { + if err := driver.Register("vsphere", InitFunc); err != nil { + fmt.Fprintf(os.Stderr, "Failed to initialize driver. Error : %s", err.Error()) + os.Exit(1) + } + if err := driver.RegisterConfig("vsphere", 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(mc) + if _, ok := err.(*errors.GovcNotFoundError); ok { + return nil, err + } + + if err != nil && mc.Init == true { + return CreateMachine(mc) + } + return m, err +} + +// Add cmdline params for this driver +func ConfigFlags(B2D *driver.MachineConfig, flags *flag.FlagSet) error { + flags.StringVar(&cfg.Govc, "govc", "govc", "Path to GOVC Binary") + flags.StringVar(&cfg.VcenterIp, "vcenter-ip", "", "vCenter URL") + flags.StringVar(&cfg.VcenterUser, "vcenter-user", "", "vCenter User") + flags.StringVar(&cfg.VcenterDC, "vcenter-datacenter", "", "vCenter Datacenter") + flags.StringVar(&cfg.VcenterDS, "vcenter-datastore", "", "vCenter Datastore") + flags.StringVar(&cfg.VcenterNet, "vcenter-vm-network", "", "vCenter VM Network") + flags.StringVar(&cfg.VcenterPool, "vcenter-pool", "", "vCenter Target Resource Pool") + flags.StringVar(&cfg.VcenterHostIp, "vcenter-host-ip", "", "vCenter Target Host IP") + + return nil +} + +// GetMachine fetches the machine information from a vCenter +func GetMachine(mc *driver.MachineConfig) (*Machine, error) { + err := GetDriverCfg(mc) + if err != nil { + return nil, err + } + + if mc.Init == false { + fmt.Fprintf(os.Stdout, "Connecting to vSphere environment %s...\n", cfg.VcenterIp) + } + + vcConn := NewVcConn(&cfg) + err = vcConn.Login() + if err != nil { + return nil, err + } + + stdout, err := vcConn.VmInfo(mc.VM) + if err != nil { + return nil, err + } + + m := &Machine{ + Name: mc.VM, + State: driver.Poweroff, + SshPubKey: mc.SSHKey + ".pub", + VcenterIp: cfg.VcenterIp, + VcenterUser: cfg.VcenterUser, + Datacenter: cfg.VcenterDC, + Network: cfg.VcenterNet, + } + + ParseVmProperty(stdout, m) + + return m, nil +} + +// create a new machine in vsphere includes the following steps: +// 1. create a directory in vsphere datastore to include the B2D ISO; +// 2. uploads the ISO to the corresponding datastore; +// 3. bootup the virtual machine with the ISO mounted; +func CreateMachine(mc *driver.MachineConfig) (*Machine, error) { + err := GetDriverCfg(mc) + if err != nil { + return nil, err + } + + vcConn := NewVcConn(&cfg) + err = vcConn.DatastoreMkdir(DATASTORE_DIR) + if err != nil { + return nil, err + } + + err = vcConn.DatastoreUpload(mc.ISO) + if err != nil { + return nil, err + } + + memory := strconv.Itoa(int(mc.Memory)) + isoPath := fmt.Sprintf("%s/%s", DATASTORE_DIR, DATASTORE_ISO_NAME) + err = vcConn.VmCreate(isoPath, memory, mc.VM) + if err != nil { + return nil, err + } + + fmt.Fprintf(os.Stdout, "Configuring the virtual machine %s... ", mc.VM) + diskSize := strconv.Itoa(int(mc.DiskSize)) + err = vcConn.VmDiskCreate(mc.VM, diskSize) + if err != nil { + fmt.Fprintf(os.Stderr, "failed!\n") + return nil, err + } + + err = vcConn.VmAttachNetwork(mc.VM) + if err != nil { + fmt.Fprintf(os.Stderr, "failed!\n") + return nil, err + } + + fmt.Fprintf(os.Stdout, "ok!\n") + cpu, err := strconv.ParseUint(cfg.Cpu, 10, 32) + if err != nil { + return nil, err + } + + m := &Machine{ + Name: mc.VM, + State: driver.Poweroff, + CPUs: uint(cpu), + Memory: mc.Memory, + VcenterIp: cfg.VcenterIp, + VcenterUser: cfg.VcenterUser, + Datacenter: cfg.VcenterDC, + Network: cfg.VcenterNet, + SshPubKey: mc.SSHKey + ".pub", + } + return m, nil +} + +func ParseVmProperty(stdout string, m *Machine) { + currentCpu := strings.Trim(strings.Split(strings.Split(stdout, "CPU:")[1], "vCPU")[0], " ") + if cpus, err := strconv.ParseUint(currentCpu, 10, 32); err == nil { + m.CPUs = uint(cpus) + } + currentMem := strings.Trim(strings.Split(strings.Split(stdout, "Memory:")[1], "MB")[0], " ") + if mem, err := strconv.ParseUint(currentMem, 10, 32); err == nil { + m.Memory = uint(mem) + } + if strings.Contains(stdout, "poweredOn") { + m.State = driver.Running + m.VmIp = strings.Trim(strings.Trim(strings.Split(stdout, "IP address:")[1], " "), "\n") + } +} + +func GetDriverCfg(mc *driver.MachineConfig) error { + vcenterIp := mc.DriverCfg["vsphere"].(map[string]interface{})["VcenterIp"] + if vcenterIp == nil { + if cfg.VcenterIp == "" { + return errors.NewIncompleteVcConfigError("vCenter IP") + } + } else { + cfg.VcenterIp = vcenterIp.(string) + } + vcenterUser := mc.DriverCfg["vsphere"].(map[string]interface{})["VcenterUser"] + if vcenterUser == nil { + if cfg.VcenterUser == "" { + return errors.NewIncompleteVcConfigError("vCenter User") + } + } else { + cfg.VcenterUser = vcenterUser.(string) + } + vcenterDC := mc.DriverCfg["vsphere"].(map[string]interface{})["VcenterDatacenter"] + if vcenterDC == nil { + if cfg.VcenterDC == "" { + return errors.NewIncompleteVcConfigError("vCenter Datacenter") + } + } else { + cfg.VcenterDC = vcenterDC.(string) + } + vcenterDS := mc.DriverCfg["vsphere"].(map[string]interface{})["VcenterDatastore"] + if vcenterDS == nil { + if cfg.VcenterDS == "" { + return errors.NewIncompleteVcConfigError("vCenter Datastore") + } + } else { + cfg.VcenterDS = vcenterDS.(string) + } + vcenterNet := mc.DriverCfg["vsphere"].(map[string]interface{})["VcenterNetwork"] + if vcenterNet == nil { + if cfg.VcenterNet == "" { + return errors.NewIncompleteVcConfigError("vCenter Network") + } + } else { + cfg.VcenterNet = vcenterNet.(string) + } + cpu := mc.DriverCfg["vsphere"].(map[string]interface{})["VmCPU"] + if cpu == nil { + if cfg.Cpu == "" { + cfg.Cpu = strconv.Itoa(DEFAULT_CPU_NUMBER) + } + } else { + cfg.Cpu = strconv.Itoa(int(cpu.(int64))) + } + + // govc path information are optional as user may want to use the default + govc := mc.DriverCfg["vsphere"].(map[string]interface{})["Govc"] + if govc != nil { + cfg.Govc = govc.(string) + } + + // vcenter resource pool and host ip are nullable configurations + pool := mc.DriverCfg["vsphere"].(map[string]interface{})["VcenterPool"] + if pool != nil { + cfg.VcenterPool = pool.(string) + } + hostIp := mc.DriverCfg["vsphere"].(map[string]interface{})["VcenterHostIp"] + if hostIp != nil { + cfg.VcenterHostIp = hostIp.(string) + } + return nil +} + +// Machine information. +type Machine struct { + Name string + State driver.MachineState + CPUs uint + Memory uint + VcenterIp string // the vcenter the machine belongs to + VcenterUser string // the vcenter user/admin to own the machine + Datacenter string // the datacenter the machine locates + Network string // the network the machine is using + VmIp string // the Ip address of the machine + SshPubKey string // pass SSH here so the vm knows the source of authorized_keys +} + +// Refresh reloads the machine information. +func (m *Machine) Refresh() error { + vcConn := NewVcConn(&cfg) + stdout, err := vcConn.VmInfo(m.Name) + if err != nil { + return err + } + ParseVmProperty(stdout, m) + return nil +} + +// Start starts the machine. +// for vSphere driver, the start process includes the following changes +// 1. start the docker virtual machine; +// 2. fetch the ip address from the virtual machine (with open-vmtools); +// 3. upload the ssh key to the virtual machine; +func (m *Machine) Start() error { + switch m.State { + case driver.Running: + msg := fmt.Sprintf("VM %s has already been started", m.Name) + fmt.Println(msg) + return nil + case driver.Poweroff: + // TODO add transactional or error handling in the following steps + vcConn := NewVcConn(&cfg) + err := vcConn.VmPowerOn(m.Name) + if err != nil { + return err + } + // this step waits for the vm to start and fetch its ip address; + // this guarantees that the opem-vmtools has started working... + _, err = vcConn.VmFetchIp(m.Name) + if err != nil { + return err + } + + fmt.Fprintf(os.Stdout, "Configuring virtual machine %s... ", m.Name) + err = vcConn.GuestMkdir("docker", "tcuser", m.Name, "/home/docker/.ssh") + if err != nil { + fmt.Fprintf(os.Stdout, "failed!\n") + return err + } + err = vcConn.GuestUpload("docker", "tcuser", m.Name, m.SshPubKey, + "/home/docker/.ssh/authorized_keys") + if err != nil { + fmt.Fprintf(os.Stdout, "failed!\n") + return err + } + fmt.Fprintf(os.Stdout, "ok!\n") + } + return nil +} + +// Suspend suspends the machine and saves its state to disk. +func (m *Machine) Save() error { + return driver.ErrNotSupported +} + +// Pause pauses the execution of the machine. +func (m *Machine) Pause() error { + return driver.ErrNotSupported +} + +// Currently make stop equivalent to poweroff as there is no shutdown guestOS API +// yet with current open-vmtools and govc +func (m *Machine) Stop() error { + vcConn := NewVcConn(&cfg) + err := vcConn.VmPowerOff(m.Name) + if err != nil { + return err + } + m.State = driver.Poweroff + return err +} + +// Poweroff forcefully stops the machine. State is lost and might corrupt the disk image. +func (m *Machine) Poweroff() error { + vcConn := NewVcConn(&cfg) + err := vcConn.VmPowerOff(m.Name) + if err != nil { + return err + } + m.State = driver.Poweroff + return err +} + +// Restart gracefully restarts the machine. +func (m *Machine) Restart() error { + switch m.State { + case driver.Running: + if err := m.Stop(); err != nil { + return err + } + case driver.Poweroff: + fmt.Fprintf(os.Stdout, "Machine %s already stopped, starting it... \n", m.Name) + } + return m.Start() +} + +// Reset forcefully restarts the machine. State is lost and might corrupt the disk image. +func (m *Machine) Reset() error { + return m.Restart() +} + +// Get current name +func (m *Machine) GetName() string { + return m.Name +} + +// Get machine address +func (m *Machine) GetAddr() string { + return m.VmIp +} + +// 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 { + if m.State == driver.Running { + msg := fmt.Sprintf("Please poweroff machine %s before delete", m.Name) + fmt.Println(msg) + return errors.NewInvalidStateError(m.Name) + } + vcConn := NewVcConn(&cfg) + err := vcConn.VmDestroy(m.Name) + if err != nil { + return err + } + return nil +} + +// Modify changes the settings of the machine. +func (m *Machine) Modify() error { + fmt.Printf("Modify %s: %s\n", m.Name, m.State) + 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 +} diff --git a/vsphere/vcenter.go b/vsphere/vcenter.go new file mode 100644 index 0000000..eaec694 --- /dev/null +++ b/vsphere/vcenter.go @@ -0,0 +1,311 @@ +package vsphere + +import ( + "fmt" + "os" + "strings" + + "github.com/boot2docker/boot2docker-cli/vsphere/errors" + "github.com/howeyc/gopass" +) + +type VcConn struct { + cfg *DriverCfg + password string +} + +func NewVcConn(cfg *DriverCfg) VcConn { + return VcConn{ + cfg: cfg, + password: "", + } +} + +func (conn VcConn) Login() error { + err := conn.queryAboutInfo() + if err == nil { + return nil + } + if _, ok := err.(*errors.GovcNotFoundError); ok { + return err + } + + fmt.Fprintf(os.Stdout, "Enter vCenter Password: ") + password := gopass.GetPasswd() + conn.password = string(password[:]) + + err = conn.queryAboutInfo() + if err == nil { + return nil + } + return err +} + +func (conn VcConn) DatastoreLs(path string) (string, error) { + args := []string{"datastore.ls"} + args = conn.AppendConnectionString(args) + args = append(args, fmt.Sprintf("--ds=%s", conn.cfg.VcenterDS)) + args = append(args, path) + stdout, stderr, err := govcOutErr(args...) + if stderr == "" && err == nil { + return stdout, nil + } + return "", errors.NewDatastoreError(conn.cfg.VcenterDC, "ls", stderr) +} + +func (conn VcConn) DatastoreMkdir(dirName string) error { + _, err := conn.DatastoreLs(dirName) + if err == nil { + return nil + } + + fmt.Fprintf(os.Stdout, "Creating directory %s on datastore %s of vCenter %s... ", + dirName, conn.cfg.VcenterDS, conn.cfg.VcenterIp) + + args := []string{"datastore.mkdir"} + args = conn.AppendConnectionString(args) + args = append(args, fmt.Sprintf("--ds=%s", conn.cfg.VcenterDS)) + args = append(args, dirName) + _, stderr, err := govcOutErr(args...) + if stderr == "" && err == nil { + fmt.Fprintf(os.Stdout, "ok!\n") + return nil + } else { + fmt.Fprintf(os.Stderr, "failed!\n") + return errors.NewDatastoreError(conn.cfg.VcenterDS, "mkdir", stderr) + } +} + +func (conn VcConn) DatastoreUpload(localPath string) error { + stdout, err := conn.DatastoreLs(DATASTORE_DIR) + if err == nil && strings.Contains(stdout, DATASTORE_ISO_NAME) { + fmt.Fprintf(os.Stdout, "boot2docker ISO already uploaded, skipping upload... \n") + return nil + } + + fmt.Fprintf(os.Stdout, "Uploading %s to %s on datastore %s of vCenter %s... ", + localPath, DATASTORE_DIR, conn.cfg.VcenterDS, conn.cfg.VcenterIp) + + dsPath := fmt.Sprintf("%s/%s", DATASTORE_DIR, DATASTORE_ISO_NAME) + args := []string{"datastore.upload"} + args = conn.AppendConnectionString(args) + args = append(args, fmt.Sprintf("--ds=%s", conn.cfg.VcenterDS)) + args = append(args, localPath) + args = append(args, dsPath) + _, stderr, err := govcOutErr(args...) + if stderr == "" && err == nil { + fmt.Fprintf(os.Stdout, "ok!\n") + return nil + } else { + fmt.Fprintf(os.Stderr, "failed!\n") + return errors.NewDatastoreError(conn.cfg.VcenterDC, "upload", stderr) + } +} + +func (conn VcConn) VmInfo(vmName string) (string, error) { + args := []string{"vm.info"} + args = conn.AppendConnectionString(args) + args = append(args, fmt.Sprintf("--dc=%s", conn.cfg.VcenterDC)) + args = append(args, vmName) + + stdout, stderr, err := govcOutErr(args...) + if strings.Contains(stdout, "Name") && stderr == "" && err == nil { + return stdout, nil + } else { + return "", errors.NewVmError("find", vmName, "VM not found") + } +} + +func (conn VcConn) VmCreate(isoPath, memory, vmName string) error { + fmt.Fprintf(os.Stdout, "Creating virtual machine %s of vCenter %s... ", + vmName, conn.cfg.VcenterIp) + + args := []string{"vm.create"} + args = conn.AppendConnectionString(args) + args = append(args, fmt.Sprintf("--net=%s", conn.cfg.VcenterNet)) + args = append(args, fmt.Sprintf("--dc=%s", conn.cfg.VcenterDC)) + args = append(args, fmt.Sprintf("--ds=%s", conn.cfg.VcenterDS)) + args = append(args, fmt.Sprintf("--iso=%s", isoPath)) + args = append(args, fmt.Sprintf("--m=%s", memory)) + args = append(args, fmt.Sprintf("--c=%s", conn.cfg.Cpu)) + args = append(args, "--disk.controller=scsi") + args = append(args, "--on=false") + if conn.cfg.VcenterPool != "" { + args = append(args, fmt.Sprintf("--pool=%s", conn.cfg.VcenterPool)) + } + if conn.cfg.VcenterHostIp != "" { + args = append(args, fmt.Sprintf("--host.ip=%s", conn.cfg.VcenterHostIp)) + } + args = append(args, vmName) + _, stderr, err := govcOutErr(args...) + + if stderr == "" && err == nil { + fmt.Fprintf(os.Stdout, "ok!\n") + return nil + } else { + fmt.Fprintf(os.Stderr, "failed!\n") + return errors.NewVmError("create", vmName, stderr) + } +} + +func (conn VcConn) VmPowerOn(vmName string) error { + fmt.Fprintf(os.Stdout, "Powering on virtual machine %s of vCenter %s... ", + vmName, conn.cfg.VcenterIp) + + args := []string{"vm.power"} + args = conn.AppendConnectionString(args) + args = append(args, "-on") + args = append(args, vmName) + _, stderr, err := govcOutErr(args...) + + if stderr == "" && err == nil { + fmt.Fprintf(os.Stdout, "ok!\n") + return nil + } else { + fmt.Fprintf(os.Stderr, "failed!\n") + return errors.NewVmError("power on", vmName, stderr) + } +} + +func (conn VcConn) VmPowerOff(vmName string) error { + fmt.Fprintf(os.Stdout, "Powering off virtual machine %s of vCenter %s... ", + vmName, conn.cfg.VcenterIp) + + args := []string{"vm.power"} + args = conn.AppendConnectionString(args) + args = append(args, "-off") + args = append(args, vmName) + _, stderr, err := govcOutErr(args...) + + if stderr == "" && err == nil { + fmt.Fprintf(os.Stdout, "ok!\n") + return nil + } else { + fmt.Fprintf(os.Stderr, "failed!\n") + return errors.NewVmError("power on", vmName, stderr) + } +} + +func (conn VcConn) VmDestroy(vmName string) error { + fmt.Fprintf(os.Stdout, "Deleting virtual machine %s of vCenter %s... ", + vmName, conn.cfg.VcenterIp) + + args := []string{"vm.destroy"} + args = conn.AppendConnectionString(args) + args = append(args, fmt.Sprintf("--dc=%s", conn.cfg.VcenterDC)) + args = append(args, vmName) + _, stderr, err := govcOutErr(args...) + + if stderr == "" && err == nil { + fmt.Fprintf(os.Stdout, "ok!\n") + return nil + } else { + fmt.Fprintf(os.Stderr, "failed!\n") + return errors.NewVmError("delete", vmName, stderr) + } + +} + +func (conn VcConn) VmDiskCreate(vmName, diskSize string) error { + args := []string{"vm.disk.create"} + args = conn.AppendConnectionString(args) + args = append(args, fmt.Sprintf("--vm=%s", vmName)) + args = append(args, fmt.Sprintf("--ds=%s", conn.cfg.VcenterDS)) + args = append(args, fmt.Sprintf("--name=%s", vmName)) + args = append(args, fmt.Sprintf("--size=%sMiB", diskSize)) + + _, stderr, err := govcOutErr(args...) + if stderr == "" && err == nil { + return nil + } else { + return errors.NewVmError("add network", vmName, stderr) + } +} + +func (conn VcConn) VmAttachNetwork(vmName string) error { + args := []string{"vm.network.add"} + args = conn.AppendConnectionString(args) + args = append(args, fmt.Sprintf("--vm=%s", vmName)) + args = append(args, fmt.Sprintf("--net=%s", conn.cfg.VcenterNet)) + + _, stderr, err := govcOutErr(args...) + if stderr == "" && err == nil { + return nil + } else { + return errors.NewVmError("add network", vmName, stderr) + } +} + +func (conn VcConn) VmFetchIp(vmName string) (string, error) { + fmt.Fprintf(os.Stdout, "Fetching IP on virtual machine %s of vCenter %s... ", + vmName, conn.cfg.VcenterIp) + + args := []string{"vm.ip"} + args = conn.AppendConnectionString(args) + args = append(args, vmName) + stdout, stderr, err := govcOutErr(args...) + + if stderr == "" && err == nil { + fmt.Fprintf(os.Stdout, "ok!\n") + return stdout, nil + } else { + fmt.Fprintf(os.Stderr, "failed!\n") + return "", errors.NewVmError("fetching IP", vmName, stderr) + } +} + +func (conn VcConn) GuestMkdir(guestUser, guestPass, vmName, dirName string) error { + args := []string{"guest.mkdir"} + args = conn.AppendConnectionString(args) + args = append(args, fmt.Sprintf("--l=%s:%s", guestUser, guestPass)) + args = append(args, fmt.Sprintf("--vm=%s", vmName)) + args = append(args, "-p") + args = append(args, dirName) + _, stderr, err := govcOutErr(args...) + + if stderr == "" && err == nil { + return nil + } else { + return errors.NewGuestError("mkdir", vmName, stderr) + } +} + +func (conn VcConn) GuestUpload(guestUser, guestPass, vmName, localPath, remotePath string) error { + args := []string{"guest.upload"} + args = conn.AppendConnectionString(args) + args = append(args, fmt.Sprintf("--l=%s:%s", guestUser, guestPass)) + args = append(args, fmt.Sprintf("--vm=%s", vmName)) + args = append(args, "-f") + args = append(args, localPath) + args = append(args, remotePath) + _, stderr, err := govcOutErr(args...) + + if stderr == "" && err == nil { + return nil + } else { + return errors.NewGuestError("upload", vmName, stderr) + } +} + +func (conn VcConn) AppendConnectionString(args []string) []string { + if conn.password == "" { + args = append(args, fmt.Sprintf("--u=%s@%s", conn.cfg.VcenterUser, cfg.VcenterIp)) + } else { + args = append(args, fmt.Sprintf("--u=%s:%s@%s", conn.cfg.VcenterUser, conn.password, conn.cfg.VcenterIp)) + } + args = append(args, "--k=true") + return args +} + +func (conn VcConn) queryAboutInfo() error { + args := []string{"about"} + args = conn.AppendConnectionString(args) + stdout, _, err := govcOutErr(args...) + if strings.Contains(stdout, "Name") { + return nil + } + if _, ok := err.(*errors.GovcNotFoundError); ok { + return err + } + return errors.NewInvalidLoginError() +}