diff --git a/drivers/hyperv/hyperv.go b/drivers/hyperv/hyperv.go new file mode 100644 index 0000000000..6a1b131454 --- /dev/null +++ b/drivers/hyperv/hyperv.go @@ -0,0 +1,521 @@ +package hyperv + +import ( + "archive/tar" + "bytes" + "fmt" + "io" + "io/ioutil" + "net/http" + "os" + "os/exec" + "path" + "time" + + "github.com/docker/docker/hosts/drivers" + "github.com/docker/docker/hosts/ssh" + "github.com/docker/docker/hosts/state" + "github.com/docker/docker/pkg/log" + flag "github.com/docker/docker/pkg/mflag" + "github.com/docker/docker/utils" +) + +type Driver struct { + storePath string + boot2DockerURL string + boot2DockerLoc string + vSwitch string + MachineName string + diskImage string + diskSize int + memSize int +} + +type CreateFlags struct { + boot2DockerURL *string + boot2DockerLoc *string + vSwitch *string + diskSize *int + memSize *int +} + +func init() { + drivers.Register("hyper-v", &drivers.RegisteredDriver{ + New: NewDriver, + RegisterCreateFlags: RegisterCreateFlags, + }) +} + +// RegisterCreateFlags registers the flags this driver adds to +// "docker hosts create" +func RegisterCreateFlags(cmd *flag.FlagSet) interface{} { + createFlags := new(CreateFlags) + createFlags.boot2DockerURL = cmd.String([]string{"-hyper-v-boot2docker-url"}, "", "The URL of the boot2docker image. Defaults to the latest available version.") + createFlags.boot2DockerLoc = cmd.String([]string{"-hyper-v-boot2docker-location"}, "", "Local boot2docker iso. Overrides URL.") + createFlags.vSwitch = cmd.String([]string{"-hyper-v-virtual-switch"}, "", "Name of virtual switch. Defaults to first found.") + createFlags.diskSize = cmd.Int([]string{"-hyper-v-disk-size"}, 20000, "Size of disk for host in MB.") + createFlags.memSize = cmd.Int([]string{"-hyper-v-memory"}, 1024, "Size of memory for host in MB.") + return createFlags +} + +func NewDriver(storePath string) (drivers.Driver, error) { + return &Driver{storePath: storePath}, nil +} + +func (d *Driver) DriverName() string { + return "hyper-v" +} + +func (d *Driver) GetURL() (string, error) { + ip, err := d.GetIP() + if err != nil { + return "", err + } + if ip == "" { + return "", nil + } + // No security for now, expect boot2docker running insecure + return fmt.Sprintf("tcp://%s:2375", ip), nil +} + +func (d *Driver) GetState() (state.State, error) { + + command := []string{ + "(", + "Get-VM", + "-Name", d.MachineName, + ").state"} + stdout, err := execute(command) + if err != nil { + return state.None, fmt.Errorf("Failed to find the VM status") + } + resp := parseStdout(stdout) + + if len(resp) < 1 { + return state.None, nil + } + switch resp[0] { + case "Running": + return state.Running, nil + case "Off": + return state.Stopped, nil + } + return state.None, nil +} + +func copyFile(inFile, outFile string) error { + in, err := os.Open(inFile) + if err != nil { + return err + } + defer in.Close() + out, err := os.Create(outFile) + if err != nil { + return err + } + defer out.Close() + _, err = io.Copy(out, in) + if err != nil { + return err + } + err = out.Sync() + return err +} + +func (d *Driver) Create() error { + err := hypervAvailable() + if err != nil { + return err + } + + d.setMachineNameIfNotSet() + + var isoURL string + + if d.boot2DockerLoc == "" { + if d.boot2DockerURL != "" { + isoURL = d.boot2DockerURL + } else { + // HACK: Docker 1.3 boot2docker image + isoURL = "http://cl.ly/1c1c0O3N193A/download/boot2docker-1.2.0-dev.iso" + // isoURL, err = getLatestReleaseURL() + // if err != nil { + // return err + // } + } + log.Infof("Downloading boot2docker...") + + if err := downloadISO(d.storePath, "boot2docker.iso", isoURL); err != nil { + return err + } + } else { + copyFile(d.boot2DockerLoc, path.Join(d.storePath, "boot2docker.iso")) + } + + log.Infof("Creating SSH key...") + + if err := ssh.GenerateSSHKey(d.sshKeyPath()); err != nil { + return err + } + + log.Infof("Creating VM...") + + virtualSwitch, err := d.chooseVirtualSwitch() + if err != nil { + return err + } + + err = d.generateDiskImage() + if err != nil { + return err + } + + command := []string{ + "New-VM", + "-Name", d.MachineName, + "-Path", fmt.Sprintf("'%s'", d.storePath), + "-MemoryStartupBytes", fmt.Sprintf("%dMB", d.memSize)} + _, err = execute(command) + if err != nil { + return err + } + + command = []string{ + "Set-VMDvdDrive", + "-VMName", d.MachineName, + "-Path", fmt.Sprintf("'%s'", path.Join(d.storePath, "boot2docker.iso"))} + _, err = execute(command) + if err != nil { + return err + } + + command = []string{ + "Add-VMHardDiskDrive", + "-VMName", d.MachineName, + "-Path", fmt.Sprintf("'%s'", d.diskImage)} + _, err = execute(command) + if err != nil { + return err + } + + command = []string{ + "Connect-VMNetworkAdapter", + "-VMName", d.MachineName, + "-SwitchName", fmt.Sprintf("'%s'", virtualSwitch)} + _, err = execute(command) + if err != nil { + return err + } + + log.Infof("Starting VM...") + return d.Start() +} + +func (d *Driver) chooseVirtualSwitch() (string, error) { + if d.vSwitch != "" { + return d.vSwitch, nil + } + command := []string{ + "@(Get-VMSwitch).Name"} + stdout, err := execute(command) + if err != nil { + return "", err + } + switches := parseStdout(stdout) + if len(switches) > 0 { + log.Infof("Using switch %s", switches[0]) + return switches[0], nil + } + return "", fmt.Errorf("no vswitch found") +} + +func (d *Driver) SetConfigFromFlags(flagsInterface interface{}) error { + flags := flagsInterface.(*CreateFlags) + d.boot2DockerURL = *flags.boot2DockerURL + d.boot2DockerLoc = *flags.boot2DockerLoc + d.vSwitch = *flags.vSwitch + d.diskSize = *flags.diskSize + d.memSize = *flags.memSize + return nil +} + +func (d *Driver) wait() error { + log.Infof("Waiting for host to start...") + for { + ip, _ := d.GetIP() + if ip != "" { + break + } + time.Sleep(1 * time.Second) + } + log.Infof("Got IP, waiting for SSH") + ip, _ := d.GetIP() + return ssh.WaitForTCP(fmt.Sprintf("%s:22", ip)) +} + +func (d *Driver) Start() error { + command := []string{ + "Start-VM", + "-Name", d.MachineName} + _, err := execute(command) + if err != nil { + return err + } + return d.wait() +} + +func (d *Driver) Stop() error { + command := []string{ + "Stop-VM", + "-Name", d.MachineName} + _, err := execute(command) + if err != nil { + return err + } + for { + s, err := d.GetState() + if err != nil { + return err + } + if s == state.Running { + time.Sleep(1 * time.Second) + } else { + break + } + } + return nil +} + +func (d *Driver) Remove() error { + s, err := d.GetState() + if err != nil { + return err + } + if s == state.Running { + if err := d.Kill(); err != nil { + return err + } + } + command := []string{ + "Remove-VM", + "-Name", d.MachineName, + "-Force"} + _, err = execute(command) + return err +} + +func (d *Driver) Restart() error { + err := d.Stop() + if err != nil { + return err + } + + return d.Start() +} + +func (d *Driver) Kill() error { + command := []string{ + "Stop-VM", + "-Name", d.MachineName, + "-TurnOff"} + _, err := execute(command) + if err != nil { + return err + } + for { + s, err := d.GetState() + if err != nil { + return err + } + if s == state.Running { + time.Sleep(1 * time.Second) + } else { + break + } + } + return nil +} + +func (d *Driver) setMachineNameIfNotSet() { + if d.MachineName == "" { + d.MachineName = fmt.Sprintf("docker-host-%s", utils.TruncateID(utils.GenerateRandomID())) + } +} + +func (d *Driver) GetIP() (string, error) { + command := []string{ + "((", + "Get-VM", + "-Name", d.MachineName, + ").networkadapters[0]).ipaddresses[0]"} + stdout, err := execute(command) + if err != nil { + return "", err + } + resp := parseStdout(stdout) + if len(resp) < 1 { + return "", fmt.Errorf("IP not found") + } + return resp[0], nil +} + +func (d *Driver) GetSSHCommand(args ...string) *exec.Cmd { + ip, _ := d.GetIP() + return ssh.GetSSHCommand(ip, 22, "docker", d.sshKeyPath(), args...) +} + +func (d *Driver) sshKeyPath() string { + return path.Join(d.storePath, "id_rsa") +} + +func (d *Driver) publicSSHKeyPath() string { + return d.sshKeyPath() + ".pub" +} + +// Get the latest boot2docker release tag name (e.g. "v0.6.0"). +// FIXME: find or create some other way to get the "latest release" of boot2docker since the GitHub API has a pretty low rate limit on API requests +// func getLatestReleaseURL() (string, error) { +// rsp, err := http.Get("https://api.github.com/repos/boot2docker/boot2docker/releases") +// if err != nil { +// return "", err +// } +// defer rsp.Body.Close() + +// var t []struct { +// TagName string `json:"tag_name"` +// } +// if err := json.NewDecoder(rsp.Body).Decode(&t); err != nil { +// return "", err +// } +// if len(t) == 0 { +// return "", fmt.Errorf("no releases found") +// } + +// tag := t[0].TagName +// url := fmt.Sprintf("https://github.com/boot2docker/boot2docker/releases/download/%s/boot2docker.iso", tag) +// return url, nil +// } + +// Download boot2docker ISO image for the given tag and save it at dest. +func downloadISO(dir, file, url string) error { + rsp, err := http.Get(url) + if err != nil { + return err + } + defer rsp.Body.Close() + + // Download to a temp file first then rename it to avoid partial download. + f, err := ioutil.TempFile(dir, file+".tmp") + if err != nil { + return err + } + defer os.Remove(f.Name()) + if _, err := io.Copy(f, rsp.Body); err != nil { + // TODO: display download progress? + return err + } + if err := f.Close(); err != nil { + return err + } + if err := os.Rename(f.Name(), path.Join(dir, file)); err != nil { + return err + } + return nil +} + +func (d *Driver) generateDiskImage() error { + // Create a small fixed vhd, put the tar in, + // convert to dynamic, then resize + + d.diskImage = path.Join(d.storePath, "disk.vhd") + fixed := path.Join(d.storePath, "fixed.vhd") + log.Infof("Creating VHD") + command := []string{ + "New-VHD", + "-Path", fmt.Sprintf("'%s'", fixed), + "-SizeBytes", "10MB", + "-Fixed"} + _, err := execute(command) + if err != nil { + return err + } + + tarBuf, err := d.generateTar() + if err != nil { + return err + } + + file, _ := os.OpenFile(fixed, os.O_WRONLY, 0644) + defer file.Close() + file.Seek(0, os.SEEK_SET) + _, err = file.Write(tarBuf.Bytes()) + if err != nil { + return err + } + file.Close() + + command = []string{ + "Convert-VHD", + "-Path", fmt.Sprintf("'%s'", fixed), + "-DestinationPath", fmt.Sprintf("'%s'", d.diskImage), + "-VHDType", "Dynamic"} + _, err = execute(command) + if err != nil { + return err + } + command = []string{ + "Resize-VHD", + "-Path", fmt.Sprintf("'%s'", d.diskImage), + "-SizeBytes", fmt.Sprintf("%dMB", d.diskSize)} + _, err = execute(command) + if err != nil { + return err + } + + return err +} + +// Make a boot2docker VM disk image. +// See https://github.com/boot2docker/boot2docker/blob/master/rootfs/rootfs/etc/rc.d/automount +func (d *Driver) generateTar() (*bytes.Buffer, error) { + magicString := "boot2docker, please format-me" + + buf := new(bytes.Buffer) + tw := tar.NewWriter(buf) + + // magicString first so the automount script knows to format the disk + file := &tar.Header{Name: magicString, Size: int64(len(magicString))} + if err := tw.WriteHeader(file); err != nil { + return nil, err + } + if _, err := tw.Write([]byte(magicString)); err != nil { + return nil, err + } + // .ssh/key.pub => authorized_keys + file = &tar.Header{Name: ".ssh", Typeflag: tar.TypeDir, Mode: 0700} + if err := tw.WriteHeader(file); err != nil { + return nil, err + } + pubKey, err := ioutil.ReadFile(d.publicSSHKeyPath()) + if err != nil { + return nil, err + } + file = &tar.Header{Name: ".ssh/authorized_keys", Size: int64(len(pubKey)), Mode: 0644} + if err := tw.WriteHeader(file); err != nil { + return nil, err + } + if _, err := tw.Write([]byte(pubKey)); err != nil { + return nil, err + } + file = &tar.Header{Name: ".ssh/authorized_keys2", Size: int64(len(pubKey)), Mode: 0644} + if err := tw.WriteHeader(file); err != nil { + return nil, err + } + if _, err := tw.Write([]byte(pubKey)); err != nil { + return nil, err + } + if err := tw.Close(); err != nil { + return nil, err + } + return buf, nil +} diff --git a/drivers/hyperv/powershell.go b/drivers/hyperv/powershell.go new file mode 100644 index 0000000000..3d3591a304 --- /dev/null +++ b/drivers/hyperv/powershell.go @@ -0,0 +1,61 @@ +package hyperv + +import ( + "bufio" + "bytes" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/docker/docker/pkg/log" +) + +var powershell string + +func init() { + systemPath := strings.Split(os.Getenv("PATH"), ";") + for _, path := range systemPath { + if strings.Index(path, "WindowsPowerShell") != -1 { + powershell = filepath.Join(path, "powershell.exe") + } + } +} + +func execute(args []string) (string, error) { + cmd := exec.Command(powershell, args...) + log.Debugf("[executing ==>] : %v %v", powershell, strings.Join(args, " ")) + var stdout bytes.Buffer + var stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + err := cmd.Run() + log.Debugf("[stdout =====>] : %s", stdout.String()) + log.Debugf("[stderr =====>] : %s", stderr.String()) + return stdout.String(), err +} + +func parseStdout(stdout string) []string { + s := bufio.NewScanner(strings.NewReader(stdout)) + resp := []string{} + for s.Scan() { + resp = append(resp, s.Text()) + } + return resp +} + +func hypervAvailable() error { + command := []string{ + "@(Get-Command Get-VM).ModuleName"} + stdout, err := execute(command) + if err != nil { + return err + } + resp := parseStdout(stdout) + + if resp[0] == "Hyper-V" { + return nil + } + return fmt.Errorf("Hyper-V PowerShell Module is not available") +}